rack-stream 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +3 -0
- data/.rspec +2 -0
- data/.travis.yml +20 -0
- data/.yardopts +1 -0
- data/Gemfile +37 -0
- data/Guardfile +11 -0
- data/LICENSE +8 -0
- data/README.md +282 -0
- data/Rakefile +15 -0
- data/examples/basic.ru +7 -0
- data/examples/loop.ru +20 -0
- data/examples/no_stream.ru +4 -0
- data/examples/websocket_client.rb +15 -0
- data/lib/rack/stream.rb +20 -0
- data/lib/rack/stream/app.rb +136 -0
- data/lib/rack/stream/deferrable_body.rb +50 -0
- data/lib/rack/stream/dsl.rb +18 -0
- data/lib/rack/stream/handlers.rb +98 -0
- data/rack-stream.gemspec +24 -0
- data/spec/integration/server_spec.rb +78 -0
- data/spec/integration/sinatra.ru +36 -0
- data/spec/integration/views/index.erb +36 -0
- data/spec/lib/rack/stream_spec.rb +125 -0
- data/spec/spec_helper.rb +35 -0
- data/spec/support/mock_server.rb +35 -0
- metadata +126 -0
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
bundler_args:
|
2
|
+
--without development debug darwin
|
3
|
+
|
4
|
+
before_script:
|
5
|
+
- "export DISPLAY=:99.0"
|
6
|
+
- "sh -e /etc/init.d/xvfb start"
|
7
|
+
- "sleep 3"
|
8
|
+
- "nohup bundle exec thin start -R spec/integration/sinatra.ru -p 8888 &"
|
9
|
+
- "sleep 2"
|
10
|
+
|
11
|
+
env:
|
12
|
+
- SERVER=http://localhost:8888
|
13
|
+
|
14
|
+
language: ruby
|
15
|
+
|
16
|
+
script: "bundle exec rake spec:integration && bundle exec rake spec"
|
17
|
+
|
18
|
+
rvm:
|
19
|
+
- 1.9.2
|
20
|
+
- 1.9.3
|
data/.yardopts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--no-private --protected - LICENSE
|
data/Gemfile
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
source 'http://rubygems.org'
|
2
|
+
|
3
|
+
gemspec
|
4
|
+
|
5
|
+
group :development do
|
6
|
+
gem 'yard'
|
7
|
+
gem 'guard'
|
8
|
+
gem 'guard-rspec'
|
9
|
+
gem 'guard-bundler'
|
10
|
+
end
|
11
|
+
|
12
|
+
group :development, :test do
|
13
|
+
gem 'rake'
|
14
|
+
gem 'rack-test'
|
15
|
+
gem 'rspec', '~> 2.9'
|
16
|
+
gem 'bundler'
|
17
|
+
gem 'pry'
|
18
|
+
gem 'faraday'
|
19
|
+
gem 'thin'
|
20
|
+
|
21
|
+
# integration
|
22
|
+
gem 'capybara-webkit'
|
23
|
+
gem 'em-eventsource'
|
24
|
+
gem 'em-http-request'
|
25
|
+
gem 'sinatra'
|
26
|
+
end
|
27
|
+
|
28
|
+
# debugger for 1.9 only
|
29
|
+
group :debug do
|
30
|
+
gem 'debugger'
|
31
|
+
end
|
32
|
+
|
33
|
+
# Mac specific
|
34
|
+
group :darwin do
|
35
|
+
gem 'rb-fsevent'
|
36
|
+
gem 'growl'
|
37
|
+
end
|
data/Guardfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,8 @@
|
|
1
|
+
Copyright (c) 2012, Jerry Cheung
|
2
|
+
All rights reserved.
|
3
|
+
|
4
|
+
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
5
|
+
|
6
|
+
Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
7
|
+
Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
8
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
data/README.md
ADDED
@@ -0,0 +1,282 @@
|
|
1
|
+
# rack-stream [![Build Status](https://secure.travis-ci.org/jch/rack-stream.png)](http://travis-ci.org/jch/rack-stream)
|
2
|
+
|
3
|
+
## Overview
|
4
|
+
|
5
|
+
rack-stream is middleware for building multi-protocol streaming rack endpoints.
|
6
|
+
|
7
|
+
## Example
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
# config.ru
|
11
|
+
require 'rack/stream'
|
12
|
+
|
13
|
+
class App
|
14
|
+
include Rack::Stream::DSL
|
15
|
+
|
16
|
+
def call(env)
|
17
|
+
after_open do
|
18
|
+
count = 0
|
19
|
+
EM.add_periodic_timer(1) do
|
20
|
+
if count != 3
|
21
|
+
chunk "chunky #{count}\n"
|
22
|
+
count += 1
|
23
|
+
else
|
24
|
+
# Connection isn't closed until #close is called.
|
25
|
+
# Useful if you're building a firehose API
|
26
|
+
close
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
before_close do
|
32
|
+
chunk "monkey!\n"
|
33
|
+
end
|
34
|
+
|
35
|
+
[200, {'Content-Type' => 'application/json'}, []]
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
Rack::Builder.app do
|
40
|
+
use Rack::Stream
|
41
|
+
run App
|
42
|
+
end
|
43
|
+
```
|
44
|
+
|
45
|
+
To run the example:
|
46
|
+
|
47
|
+
```
|
48
|
+
> thin start -R config.ru -p 3000
|
49
|
+
> curl -i -N http://localhost:3000/
|
50
|
+
>> HTTP/1.1 200 OK
|
51
|
+
>> Content-Type: text/plain
|
52
|
+
>> Transfer-Encoding: chunked
|
53
|
+
>>
|
54
|
+
>> chunky 0
|
55
|
+
>> chunky 1
|
56
|
+
>> chunky 2
|
57
|
+
>> monkey
|
58
|
+
```
|
59
|
+
|
60
|
+
This same endpoint can be accessed via WebSockets or EventSource, see
|
61
|
+
'Multi-Protocol Support' below.
|
62
|
+
|
63
|
+
## Connection Lifecycle
|
64
|
+
|
65
|
+
When using rack-stream, downstream apps can access the
|
66
|
+
`Rack::Stream::App` instance via `env['rack.stream']`. This object is
|
67
|
+
used to control when the connection is closed, and what is streamed.
|
68
|
+
`Rack::Stream::DSL` delegates access methods to `env['rack.stream']`
|
69
|
+
on the downstream rack app.
|
70
|
+
|
71
|
+
`Rack::Stream::App` instances are in one of the follow states:
|
72
|
+
|
73
|
+
* new
|
74
|
+
* open
|
75
|
+
* closed
|
76
|
+
* errored
|
77
|
+
|
78
|
+
Each state is described below.
|
79
|
+
|
80
|
+
### new
|
81
|
+
|
82
|
+
When a request first comes in, rack-stream processes any downstream
|
83
|
+
rack apps and uses their status and headers for its response. Any
|
84
|
+
downstream response bodies are queued for streaming once the headers
|
85
|
+
and status have been sent.
|
86
|
+
|
87
|
+
```ruby
|
88
|
+
use Rack::Stream
|
89
|
+
|
90
|
+
# once Rack::Stream instance is :open, 'Chunky Monkey' will be streamed out
|
91
|
+
run lambda {|env| [200, {'Content-Type' => 'text/plain'}, ['Chunky Monkey']]}
|
92
|
+
```
|
93
|
+
|
94
|
+
### open
|
95
|
+
|
96
|
+
Before the status and headers are sent in the response, they are
|
97
|
+
frozen and cannot be further modified. Attempting to modify these
|
98
|
+
fields will put the instance into an `:errored` state.
|
99
|
+
|
100
|
+
After the status and headers are sent, registered `:after_open`
|
101
|
+
callbacks will be called. If no `:after_open` callbacks are defined,
|
102
|
+
the instance will close the connection after flushing any queued
|
103
|
+
chunks.
|
104
|
+
|
105
|
+
If any `:after_open` callbacks are defined, it's the callback's
|
106
|
+
responsibility to call `#close` when the connection should be
|
107
|
+
closed. This allows you to build firehose streaming APIs with full
|
108
|
+
control of when to close connections.
|
109
|
+
|
110
|
+
```ruby
|
111
|
+
use Rack::Stream
|
112
|
+
|
113
|
+
run lambda {|env|
|
114
|
+
stream = env['rack.stream']
|
115
|
+
stream.after_open do
|
116
|
+
stream.chunk "Chunky"
|
117
|
+
stream.chunk "Monkey"
|
118
|
+
stream.close # <-- It's your responsibility to close the connection
|
119
|
+
end
|
120
|
+
[200, {'Content-Type' => 'text/plain'}, ['Hello', 'World']] # <-- downstream response bodies are also streamed
|
121
|
+
}
|
122
|
+
```
|
123
|
+
|
124
|
+
There are no `:before_open` callbacks. If you want something to be
|
125
|
+
done before streaming is started, simply return it as part of your
|
126
|
+
downstream response.
|
127
|
+
|
128
|
+
### closed
|
129
|
+
|
130
|
+
An instance enters the `:closed` state after the method `#close` is
|
131
|
+
called on it. By default, any remainined queued content to be streamed
|
132
|
+
will be flushed before the connection is closed.
|
133
|
+
|
134
|
+
```ruby
|
135
|
+
use Rack::Stream
|
136
|
+
|
137
|
+
run lambda {|env|
|
138
|
+
# to save typing, access the Rack::Stream instance with #instance_eval
|
139
|
+
env['rack.stream'].instance_eval do
|
140
|
+
before_close do
|
141
|
+
chunk "Goodbye!" # chunks can still be sent
|
142
|
+
end
|
143
|
+
|
144
|
+
after_close do
|
145
|
+
# any additional cleanup. Calling #chunk here will result in an error.
|
146
|
+
end
|
147
|
+
end
|
148
|
+
[200, {}, []]
|
149
|
+
}
|
150
|
+
```
|
151
|
+
|
152
|
+
### errored
|
153
|
+
|
154
|
+
An instance enters the `:errored` state if an illegal action is
|
155
|
+
performed in one of the states. Legal actions for the different states
|
156
|
+
are:
|
157
|
+
|
158
|
+
* **new** - `#status=`, `#headers=`
|
159
|
+
* **open** - `#chunk`, `#close`
|
160
|
+
|
161
|
+
All other actions are considered illegal. Manipulating headers after
|
162
|
+
`:new` is also illegal. The connection is closed immediately, and the
|
163
|
+
error is written to `env['rack.error']`
|
164
|
+
|
165
|
+
## Manipuating Content
|
166
|
+
|
167
|
+
When a connection is open and streaming content, you can define
|
168
|
+
`:before_chunk` callbacks to manipulate the content before it's sent
|
169
|
+
out.
|
170
|
+
|
171
|
+
```ruby
|
172
|
+
use Rack::Stream
|
173
|
+
|
174
|
+
run lambda {|env|
|
175
|
+
env['rack.stream'].instance_eval do
|
176
|
+
after_open do
|
177
|
+
chunk "chunky", "monkey"
|
178
|
+
end
|
179
|
+
|
180
|
+
before_chunk do |chunks|
|
181
|
+
# return the manipulated chunks of data to be sent
|
182
|
+
# this will stream MONKEYCHUNKY
|
183
|
+
chunks.map(&:upcase).reverse
|
184
|
+
end
|
185
|
+
end
|
186
|
+
}
|
187
|
+
```
|
188
|
+
|
189
|
+
## Multi-Protocol Support
|
190
|
+
|
191
|
+
`Rack::Stream` allows you to write an API endpoint that automatically
|
192
|
+
responds to different protocols based on the incoming request. This
|
193
|
+
allows you to write a single rack endpoint that can respond to normal
|
194
|
+
HTTP, WebSockets, or EventSource.
|
195
|
+
|
196
|
+
Assuming that rack-stream endpoint is running on port 3000. You can
|
197
|
+
access it with the following:
|
198
|
+
|
199
|
+
### HTTP
|
200
|
+
|
201
|
+
```
|
202
|
+
# -i prints headers, -N immediately displays output instead of buffering
|
203
|
+
curl -i -N http://localhost:3000/
|
204
|
+
```
|
205
|
+
|
206
|
+
### WebSockets
|
207
|
+
|
208
|
+
With Ruby:
|
209
|
+
|
210
|
+
```ruby
|
211
|
+
require 'eventmachine'
|
212
|
+
require 'faye/websocket'
|
213
|
+
|
214
|
+
EM.run {
|
215
|
+
socket = Faye::WebSocket::Client.new('ws://localhost:3000/)
|
216
|
+
socket.onmessage = lambda {|e| puts e.data} # puts each streamed chunk
|
217
|
+
socket.onclose = lambda {|e| EM.stop}
|
218
|
+
}
|
219
|
+
```
|
220
|
+
|
221
|
+
With Javascript:
|
222
|
+
|
223
|
+
```js
|
224
|
+
var socket = new WebSocket("ws://localhost:3000/");
|
225
|
+
socket.onmessage = function(m) {console.log(m);}
|
226
|
+
socket.onclose = function() {console.log('socket closed');}
|
227
|
+
```
|
228
|
+
|
229
|
+
### EventSource
|
230
|
+
|
231
|
+
From Wikipedia:
|
232
|
+
|
233
|
+
> Server-sent events is a technology for providing push notifications
|
234
|
+
> from a server to a browser client in the form of DOM events. The
|
235
|
+
> Server-Sent Events EventSource API is now being standardized as part
|
236
|
+
> of HTML5 by the W3C.
|
237
|
+
|
238
|
+
With Ruby:
|
239
|
+
|
240
|
+
```ruby
|
241
|
+
require 'em-eventsource'
|
242
|
+
|
243
|
+
EM.run do
|
244
|
+
source = EventMachine::EventSource.new("http://example.com/streaming")
|
245
|
+
source.message do |m|
|
246
|
+
puts m
|
247
|
+
end
|
248
|
+
source.start
|
249
|
+
end
|
250
|
+
```
|
251
|
+
|
252
|
+
With Javascript:
|
253
|
+
|
254
|
+
```js
|
255
|
+
var source = new EventSource('/');
|
256
|
+
source.addEventListener('message', function(e) {
|
257
|
+
console.log(e.data);
|
258
|
+
});
|
259
|
+
```
|
260
|
+
|
261
|
+
## Supported Runtimes
|
262
|
+
|
263
|
+
* 1.9.2
|
264
|
+
* 1.9.3
|
265
|
+
|
266
|
+
If a runtime is not listed above, it may still work. It just means I
|
267
|
+
haven't tried it yet.
|
268
|
+
|
269
|
+
## Roadmap
|
270
|
+
|
271
|
+
* more protocols / custom protocols http://en.wikipedia.org/wiki/HTTP_Streaming
|
272
|
+
* integrate into [grape](http://github.com/intridea/grape)
|
273
|
+
* add sinatra example that serves page that uses JS to connect
|
274
|
+
|
275
|
+
## Further Reading
|
276
|
+
|
277
|
+
* [Stream Updates With Server-Sent Events](http://www.html5rocks.com/en/tutorials/eventsource/basics/)
|
278
|
+
* [thin_async](https://github.com/macournoyer/thin_async) was where I got started
|
279
|
+
* [thin-async-test](https://github.com/phiggins/thin-async-test) used to simulate thin in tests
|
280
|
+
* [thin](https://github.com/macournoyer/thin)
|
281
|
+
* [faye-websocket-ruby](https://github.com/faye/faye-websocket-ruby) used for testing and handling different protocols
|
282
|
+
* [rack-chunked](http://rack.rubyforge.org/doc/Rack/Chunked.html)
|
data/Rakefile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
require "bundler/gem_tasks"
|
3
|
+
require "rspec/core/rake_task"
|
4
|
+
|
5
|
+
Bundler::GemHelper.install_tasks
|
6
|
+
|
7
|
+
RSpec::Core::RakeTask.new('spec') do |t|
|
8
|
+
t.rspec_opts = '--tag ~integration'
|
9
|
+
end
|
10
|
+
|
11
|
+
RSpec::Core::RakeTask.new('spec:integration') do |t|
|
12
|
+
t.pattern = 'spec/integration/*_spec.rb'
|
13
|
+
end
|
14
|
+
|
15
|
+
task :default => :spec
|
data/examples/basic.ru
ADDED
data/examples/loop.ru
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'rack/stream'
|
2
|
+
|
3
|
+
use Rack::Stream
|
4
|
+
|
5
|
+
run lambda {|env|
|
6
|
+
env["rack.stream"].instance_eval do
|
7
|
+
count = 0
|
8
|
+
after_open do
|
9
|
+
timer = EM::PeriodicTimer.new(0.1) do
|
10
|
+
if count > 10
|
11
|
+
timer.cancel
|
12
|
+
close
|
13
|
+
end
|
14
|
+
chunk "Chunky\n"
|
15
|
+
count += 1
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
[200, {}, []]
|
20
|
+
}
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'faye/websocket'
|
2
|
+
|
3
|
+
EM.run {
|
4
|
+
ws = Faye::WebSocket::Client.new('ws://localhost:3000/')
|
5
|
+
ws.onopen = lambda do |event|
|
6
|
+
ws.send("hello world")
|
7
|
+
end
|
8
|
+
ws.onmessage = lambda do |event|
|
9
|
+
puts "message: #{event.data}"
|
10
|
+
end
|
11
|
+
ws.onclose = lambda do |event|
|
12
|
+
puts "websocket closed"
|
13
|
+
EM.stop
|
14
|
+
end
|
15
|
+
}
|
data/lib/rack/stream.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'eventmachine'
|
2
|
+
require 'faye/websocket'
|
3
|
+
|
4
|
+
require 'rack/stream/handlers'
|
5
|
+
require 'rack/stream/deferrable_body'
|
6
|
+
require 'rack/stream/app'
|
7
|
+
require 'rack/stream/dsl'
|
8
|
+
|
9
|
+
module Rack
|
10
|
+
# Middleware for building multi-protocol streaming rack endpoints.
|
11
|
+
class Stream
|
12
|
+
def initialize(app, options={})
|
13
|
+
@app = app
|
14
|
+
end
|
15
|
+
|
16
|
+
def call(env)
|
17
|
+
App.new(@app).call(env)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
module Rack
|
2
|
+
class Stream
|
3
|
+
class App
|
4
|
+
class UnsupportedServerError < StandardError; end
|
5
|
+
class StateConstraintError < StandardError; end
|
6
|
+
|
7
|
+
# The state of the connection
|
8
|
+
# :new
|
9
|
+
# :open
|
10
|
+
# :closed
|
11
|
+
attr_reader :state
|
12
|
+
|
13
|
+
# @private
|
14
|
+
attr_reader :env
|
15
|
+
|
16
|
+
attr_reader :status, :headers
|
17
|
+
|
18
|
+
def initialize(app, options={})
|
19
|
+
@app = app
|
20
|
+
@state = :new
|
21
|
+
@callbacks = Hash.new {|h,k| h[k] = []}
|
22
|
+
end
|
23
|
+
|
24
|
+
def call(env)
|
25
|
+
@env = env
|
26
|
+
@env['rack.stream'] = self
|
27
|
+
|
28
|
+
app_status, app_headers, app_body = @app.call(@env)
|
29
|
+
|
30
|
+
@status = app_status
|
31
|
+
@headers = app_headers
|
32
|
+
@handler = Handlers.find(self)
|
33
|
+
|
34
|
+
# apply before_chunk to any response bodies
|
35
|
+
@callbacks[:after_open].unshift(lambda {chunk(*app_body)})
|
36
|
+
|
37
|
+
# By default, close a connection if no :after_open is specified
|
38
|
+
after_open {close} if @callbacks[:after_open].size == 1
|
39
|
+
|
40
|
+
EM.next_tick {
|
41
|
+
open!
|
42
|
+
}
|
43
|
+
ASYNC_RESPONSE
|
44
|
+
end
|
45
|
+
|
46
|
+
def status=(code)
|
47
|
+
require_state :new
|
48
|
+
@status = code
|
49
|
+
end
|
50
|
+
|
51
|
+
def headers=(hash)
|
52
|
+
require_state :new
|
53
|
+
@headers = hash
|
54
|
+
end
|
55
|
+
|
56
|
+
def chunk(*chunks)
|
57
|
+
require_state :open
|
58
|
+
run_callbacks(:chunk, chunks) {|mutated_chunks|
|
59
|
+
@handler.chunk(*mutated_chunks)
|
60
|
+
}
|
61
|
+
end
|
62
|
+
alias :<< :chunk
|
63
|
+
|
64
|
+
def close(flush = true)
|
65
|
+
require_state :open
|
66
|
+
|
67
|
+
# run in the next tick since it's more natural to call #chunk right
|
68
|
+
# before #close
|
69
|
+
EM.next_tick {
|
70
|
+
run_callbacks(:close) {
|
71
|
+
@state = :closed
|
72
|
+
@handler.close!(flush)
|
73
|
+
}
|
74
|
+
}
|
75
|
+
end
|
76
|
+
|
77
|
+
def new?; @state == :new end
|
78
|
+
def open?; @state == :open end
|
79
|
+
def closed?; @state == :closed end
|
80
|
+
def errored?; @state == :errored end
|
81
|
+
|
82
|
+
private
|
83
|
+
ASYNC_RESPONSE = [-1, {}, []].freeze
|
84
|
+
|
85
|
+
def require_state(*allowed_states)
|
86
|
+
unless allowed_states.include?(@state)
|
87
|
+
action = caller[0]
|
88
|
+
raise StateConstraintError.new("\nCalled\n '#{caller[0]}'\n Allowed :#{allowed_states * ','}\n Current :#{@state}")
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# Transition state from :new to :open
|
93
|
+
#
|
94
|
+
# Freezes headers to prevent further modification
|
95
|
+
def open! #(server)
|
96
|
+
raise UnsupportedServerError.new "missing async.callback. run within thin or rainbows" unless @env['async.callback']
|
97
|
+
run_callbacks(:open) {
|
98
|
+
@state = :open
|
99
|
+
@headers.freeze
|
100
|
+
@handler.open!
|
101
|
+
}
|
102
|
+
end
|
103
|
+
|
104
|
+
# Skips any remaining chunks, and immediately closes the connection
|
105
|
+
def error!(e)
|
106
|
+
@env['rack.errors'].puts(e.message)
|
107
|
+
@status = 500 if new?
|
108
|
+
@state = :errored
|
109
|
+
@handler.close!(false)
|
110
|
+
end
|
111
|
+
|
112
|
+
def self.define_callbacks(name, *types)
|
113
|
+
types.each do |type|
|
114
|
+
callback_name = "#{type}_#{name.to_s}"
|
115
|
+
define_method callback_name do |&blk|
|
116
|
+
@callbacks[callback_name.to_sym] << blk
|
117
|
+
self
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
define_callbacks :open, :after
|
122
|
+
define_callbacks :chunk, :before, :after
|
123
|
+
define_callbacks :close, :before, :after
|
124
|
+
|
125
|
+
def run_callbacks(name, *args)
|
126
|
+
modified = @callbacks["before_#{name}".to_sym].inject(args) do |memo, cb|
|
127
|
+
[cb.call(*memo)]
|
128
|
+
end
|
129
|
+
yield(*modified) if block_given?
|
130
|
+
@callbacks["after_#{name}".to_sym].each {|cb| cb.call(*args)}
|
131
|
+
rescue StateConstraintError => e
|
132
|
+
error!(e)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Rack
|
2
|
+
class Stream
|
3
|
+
# From [thin_async](https://github.com/macournoyer/thin_async)
|
4
|
+
class DeferrableBody
|
5
|
+
include EM::Deferrable
|
6
|
+
|
7
|
+
# @param chunks - object that responds to each. holds initial chunks of content
|
8
|
+
def initialize(chunks = [])
|
9
|
+
@queue = []
|
10
|
+
chunks.each {|c| chunk(c)}
|
11
|
+
end
|
12
|
+
|
13
|
+
# Enqueue a chunk of content to be flushed to stream at a later time
|
14
|
+
def chunk(*chunks)
|
15
|
+
@queue += chunks
|
16
|
+
schedule_dequeue
|
17
|
+
end
|
18
|
+
|
19
|
+
# When rack attempts to iterate over `body`, save the block,
|
20
|
+
# and execute at a later time when `@queue` has elements
|
21
|
+
def each(&blk)
|
22
|
+
@body_callback = blk
|
23
|
+
schedule_dequeue
|
24
|
+
end
|
25
|
+
|
26
|
+
def empty?
|
27
|
+
@queue.empty?
|
28
|
+
end
|
29
|
+
|
30
|
+
def close!(flush = true)
|
31
|
+
EM.next_tick {
|
32
|
+
succeed if !flush
|
33
|
+
succeed if flush && empty?
|
34
|
+
schedule_dequeue if flush && !empty?
|
35
|
+
}
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def schedule_dequeue
|
41
|
+
return unless @body_callback
|
42
|
+
EM.next_tick do
|
43
|
+
next unless c = @queue.shift
|
44
|
+
@body_callback.call(c)
|
45
|
+
schedule_dequeue unless empty?
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
class Stream
|
5
|
+
module DSL
|
6
|
+
def self.included(base)
|
7
|
+
base.class_eval do
|
8
|
+
extend Forwardable
|
9
|
+
def_delegators :rack_stream, :after_open, :before_chunk, :chunk, :after_chunk, :before_close, :close, :after_close
|
10
|
+
|
11
|
+
def rack_stream
|
12
|
+
env['rack.stream']
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
module Rack
|
2
|
+
class Stream
|
3
|
+
module Handlers
|
4
|
+
def find(app)
|
5
|
+
if Faye::WebSocket.websocket?(app.env)
|
6
|
+
WebSocket.new(app)
|
7
|
+
elsif Faye::EventSource.eventsource?(app.env)
|
8
|
+
EventSource.new(app)
|
9
|
+
else
|
10
|
+
Http.new(app)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
module_function :find
|
14
|
+
|
15
|
+
class AbstractHandler
|
16
|
+
def initialize(app)
|
17
|
+
@app = app
|
18
|
+
end
|
19
|
+
|
20
|
+
def chunk(*chunks)
|
21
|
+
raise NotImplementedError
|
22
|
+
end
|
23
|
+
|
24
|
+
def open!
|
25
|
+
raise NotImplementedError
|
26
|
+
end
|
27
|
+
|
28
|
+
def close!(flush = true)
|
29
|
+
raise NotImplementedError
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class Http < AbstractHandler
|
34
|
+
TERM = "\r\n".freeze
|
35
|
+
TAIL = "0#{TERM}#{TERM}".freeze
|
36
|
+
|
37
|
+
def initialize(app)
|
38
|
+
super
|
39
|
+
@app.headers['Transfer-Encoding'] = 'chunked'
|
40
|
+
@app.headers.delete('Content-Length')
|
41
|
+
@body = DeferrableBody.new # swap this out for different body types
|
42
|
+
end
|
43
|
+
|
44
|
+
def chunk(*chunks)
|
45
|
+
@body.chunk(*chunks.map {|c| encode_chunk(c)})
|
46
|
+
end
|
47
|
+
|
48
|
+
def open!
|
49
|
+
@app.env['async.callback'].call [@app.status, @app.headers, @body]
|
50
|
+
end
|
51
|
+
|
52
|
+
def close!(flush = true)
|
53
|
+
@body.chunk(TAIL) # tail is special and already encoded
|
54
|
+
@body.close!(flush)
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
def encode_chunk(c)
|
59
|
+
return nil if c.nil?
|
60
|
+
|
61
|
+
size = Rack::Utils.bytesize(c) # Rack::File?
|
62
|
+
return nil if size == 0
|
63
|
+
c.dup.force_encoding(Encoding::BINARY) if c.respond_to?(:force_encoding)
|
64
|
+
[size.to_s(16), TERM, c, TERM].join
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
class WebSocket < AbstractHandler
|
69
|
+
def chunk(*chunks)
|
70
|
+
# this is not called until after #open!, so @ws is always defined
|
71
|
+
chunks.each {|c| @ws.send(c)}
|
72
|
+
end
|
73
|
+
|
74
|
+
def close!(flush = true)
|
75
|
+
@ws.close(@app.status)
|
76
|
+
end
|
77
|
+
|
78
|
+
def open!
|
79
|
+
@ws = Faye::WebSocket.new(@app.env)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
class EventSource < WebSocket
|
84
|
+
def chunk(*chunks)
|
85
|
+
chunks.each {|c| @es.send(c)}
|
86
|
+
end
|
87
|
+
|
88
|
+
def close!(flush = true)
|
89
|
+
@es.close
|
90
|
+
end
|
91
|
+
|
92
|
+
def open!
|
93
|
+
@es = Faye::EventSource.new(@app.env)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
data/rack-stream.gemspec
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
$:.push File.expand_path("../lib", __FILE__)
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = "rack-stream"
|
5
|
+
s.version = "0.0.2"
|
6
|
+
s.platform = Gem::Platform::RUBY
|
7
|
+
s.authors = ["Jerry Cheung"]
|
8
|
+
s.email = ["jerry@intridea.com"]
|
9
|
+
s.homepage = "https://github.com/jch/rack-stream"
|
10
|
+
s.summary = %q{Rack middleware for building multi-protocol streaming rack endpoints}
|
11
|
+
s.description = %q{Rack middleware for building multi-protocol streaming rack endpoints}
|
12
|
+
s.license = "BSD"
|
13
|
+
|
14
|
+
s.rubyforge_project = "rack-stream"
|
15
|
+
|
16
|
+
s.add_runtime_dependency 'rack'
|
17
|
+
s.add_runtime_dependency 'eventmachine'
|
18
|
+
s.add_runtime_dependency 'faye-websocket'
|
19
|
+
|
20
|
+
s.files = `git ls-files`.split("\n")
|
21
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
22
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
23
|
+
s.require_paths = ["lib"]
|
24
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# CI test. see .travis.yml for config variables
|
2
|
+
if ENV['SERVER']
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'rack'
|
5
|
+
require 'rack/stream'
|
6
|
+
require 'rspec'
|
7
|
+
|
8
|
+
require 'capybara/rspec'
|
9
|
+
require 'faraday'
|
10
|
+
require 'em-eventsource'
|
11
|
+
require 'capybara'
|
12
|
+
require 'capybara-webkit'
|
13
|
+
|
14
|
+
describe 'Integration', :integration => true, :type => :request, :driver => :webkit do
|
15
|
+
EXPECTED = 'HELLO WORLDCHUNKYMONKEYBROWNIEBATTERCLOSING'.freeze
|
16
|
+
let(:uri) {URI.parse(ENV['SERVER'])}
|
17
|
+
|
18
|
+
before :all do
|
19
|
+
Capybara.app_host = uri.to_s
|
20
|
+
Capybara.run_server = false
|
21
|
+
end
|
22
|
+
|
23
|
+
describe 'HTTP' do
|
24
|
+
it 'should stream with chunked transfer encoding' do
|
25
|
+
http = Faraday.new uri.to_s
|
26
|
+
2.times.map do
|
27
|
+
res = http.get '/'
|
28
|
+
res.status.should == 200
|
29
|
+
res.headers['content-type'].should == 'text/plain'
|
30
|
+
res.headers['transfer-encoding'].should == 'chunked'
|
31
|
+
res.body.should == EXPECTED
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
describe 'WebSocket' do
|
37
|
+
it 'should stream with websockets' do
|
38
|
+
uri.scheme = 'ws'
|
39
|
+
EM.run {
|
40
|
+
ws = Faye::WebSocket::Client.new(uri.to_s)
|
41
|
+
# ws.onopen = lambda {|e| puts 'opened'}
|
42
|
+
$ws_chunks = []
|
43
|
+
ws.onmessage = lambda {|e| $ws_chunks << e.data}
|
44
|
+
ws.onclose = lambda do |e|
|
45
|
+
EM.stop
|
46
|
+
$ws_chunks.join('').should == EXPECTED
|
47
|
+
$ws_chunks = nil
|
48
|
+
end
|
49
|
+
}
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
describe 'EventSource' do
|
54
|
+
# em-eventsource needs to send 'Accept' => 'text/event-stream'
|
55
|
+
# not sure if the gem isn't working or if its rack-stream. a web integration spec would be nice
|
56
|
+
# it 'should stream with eventsource' do
|
57
|
+
# @chunks = ""
|
58
|
+
# source = EventMachine::EventSource.new(uri.to_s)
|
59
|
+
# source.message do |message|
|
60
|
+
# puts message
|
61
|
+
# @chunks << message
|
62
|
+
# source.stop if @chunks == EXPECTED
|
63
|
+
# end
|
64
|
+
# source.start
|
65
|
+
# end
|
66
|
+
end
|
67
|
+
|
68
|
+
describe 'Javascript', :type => :request, :driver => :webkit do
|
69
|
+
it 'should work from a js client' do
|
70
|
+
visit '/capybara'
|
71
|
+
# capybara doesn't distinguish between " " and ""
|
72
|
+
all('#ws li').map(&:text).should =~ ["socket opened", "HELLO", "", "WORLD", "CHUNKY", "MONKEY", "BROWNIE", "BATTER", "CLOSING", "socket closed"]
|
73
|
+
|
74
|
+
all('#es li').map(&:text).should =~ ["HELLO", "", "WORLD", "CHUNKY", "MONKEY", "BROWNIE", "BATTER", "CLOSING"]
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'sinatra/base'
|
2
|
+
require 'rack/stream'
|
3
|
+
|
4
|
+
use Rack::Stream
|
5
|
+
|
6
|
+
class App < Sinatra::Base
|
7
|
+
include Rack::Stream::DSL
|
8
|
+
|
9
|
+
get '/capybara' do
|
10
|
+
erb :index
|
11
|
+
end
|
12
|
+
|
13
|
+
get '/' do
|
14
|
+
after_open do
|
15
|
+
chunk "Chunky", "Monkey"
|
16
|
+
EM.next_tick do
|
17
|
+
chunk "Brownie", "Batter"
|
18
|
+
close
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
before_chunk do |chunks|
|
23
|
+
chunks.map(&:upcase)
|
24
|
+
end
|
25
|
+
|
26
|
+
before_close do
|
27
|
+
chunk "closing"
|
28
|
+
end
|
29
|
+
|
30
|
+
status 200
|
31
|
+
headers 'Content-Type' => 'text/plain'
|
32
|
+
['Hello', ' ', 'World']
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
run App
|
@@ -0,0 +1,36 @@
|
|
1
|
+
<html>
|
2
|
+
<head></head>
|
3
|
+
<body>
|
4
|
+
<h1>Rack::Stream Sinatra Example</h1>
|
5
|
+
<h2>Websocket</h2>
|
6
|
+
<ul id='ws'></ul>
|
7
|
+
|
8
|
+
<h2>EventSource</h2>
|
9
|
+
<ul id='es'></ul>
|
10
|
+
|
11
|
+
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
|
12
|
+
<script type='text/javascript'>
|
13
|
+
var socket, source;
|
14
|
+
|
15
|
+
function ws(m) {
|
16
|
+
$('<li>').text(m).appendTo('#ws');
|
17
|
+
}
|
18
|
+
|
19
|
+
function es(m) {
|
20
|
+
$('<li>').text(m).appendTo('#es');
|
21
|
+
}
|
22
|
+
|
23
|
+
$(function() {
|
24
|
+
socket = new WebSocket("ws://localhost:8888/");
|
25
|
+
socket.onopen = function() {ws('socket opened');}
|
26
|
+
socket.onmessage = function(m) {ws(m.data);}
|
27
|
+
socket.onclose = function() {ws('socket closed');}
|
28
|
+
|
29
|
+
source = new EventSource('http://localhost:8888/');
|
30
|
+
source.addEventListener('message', function(e) {
|
31
|
+
es(e.data);
|
32
|
+
});
|
33
|
+
});
|
34
|
+
</script>
|
35
|
+
</body>
|
36
|
+
</html>
|
@@ -0,0 +1,125 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Rack::Stream do
|
4
|
+
def app
|
5
|
+
b = Rack::Builder.new
|
6
|
+
b.use Support::MockServer
|
7
|
+
b.use Rack::Stream
|
8
|
+
b.run endpoint
|
9
|
+
b
|
10
|
+
end
|
11
|
+
|
12
|
+
shared_examples_for 'invalid action' do
|
13
|
+
it "should raise invalid state" do
|
14
|
+
get '/'
|
15
|
+
last_response.errors.should =~ /Invalid action/
|
16
|
+
last_response.status.should == 500
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
let(:endpoint) {
|
21
|
+
lambda {|env| [201, {'Content-Type' => 'text/plain', 'Content-Length' => 11}, ["Hello world"]]}
|
22
|
+
}
|
23
|
+
|
24
|
+
before {get '/'}
|
25
|
+
|
26
|
+
context "defaults" do
|
27
|
+
it "should close connection with status" do
|
28
|
+
last_response.status.should == 201
|
29
|
+
end
|
30
|
+
|
31
|
+
it "should set headers" do
|
32
|
+
last_response.headers['Content-Type'].should == 'text/plain'
|
33
|
+
end
|
34
|
+
|
35
|
+
it "should not error" do
|
36
|
+
last_response.errors.should == ""
|
37
|
+
end
|
38
|
+
|
39
|
+
it "should remove Content-Length header" do
|
40
|
+
last_response.headers['Content-Length'].should be_nil
|
41
|
+
end
|
42
|
+
|
43
|
+
it "should use chunked transfer encoding" do
|
44
|
+
last_response.headers['Transfer-Encoding'].should == 'chunked'
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
context "basic streaming" do
|
49
|
+
let(:endpoint) {
|
50
|
+
lambda {|env|
|
51
|
+
env['rack.stream'].instance_eval do
|
52
|
+
after_open do
|
53
|
+
chunk "Chunky "
|
54
|
+
chunk "Monkey"
|
55
|
+
close
|
56
|
+
end
|
57
|
+
end
|
58
|
+
[200, {'Content-Length' => 0}, ['']]
|
59
|
+
}
|
60
|
+
}
|
61
|
+
|
62
|
+
it "should stream and close" do
|
63
|
+
last_response.status.should == 200
|
64
|
+
# last_response.body.should == "Chunky Monkey"
|
65
|
+
last_response.body.should == "7\r\nChunky \r\n6\r\nMonkey\r\n0\r\n\r\n"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
context "before chunk" do
|
70
|
+
let(:endpoint) {
|
71
|
+
lambda {|env|
|
72
|
+
env['rack.stream'].instance_eval do
|
73
|
+
after_open do
|
74
|
+
chunk "Chunky", "Monkey"
|
75
|
+
close
|
76
|
+
end
|
77
|
+
|
78
|
+
before_chunk {|chunks| chunks.map(&:upcase)}
|
79
|
+
before_chunk {|chunks| chunks.reverse}
|
80
|
+
end
|
81
|
+
[200, {}, []]
|
82
|
+
}
|
83
|
+
}
|
84
|
+
|
85
|
+
it 'should allow modification of queued chunks' do
|
86
|
+
last_response.body.should == "6\r\nMONKEY\r\n6\r\nCHUNKY\r\n0\r\n\r\n"
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
context "before close" do
|
91
|
+
let(:endpoint) {
|
92
|
+
lambda {|env|
|
93
|
+
env['rack.stream'].instance_eval do
|
94
|
+
before_close do
|
95
|
+
chunk "Chunky "
|
96
|
+
chunk "Monkey"
|
97
|
+
end
|
98
|
+
end
|
99
|
+
[200, {}, []]
|
100
|
+
}
|
101
|
+
}
|
102
|
+
|
103
|
+
it "should stream and close" do
|
104
|
+
last_response.body.should == "7\r\nChunky \r\n6\r\nMonkey\r\n0\r\n\r\n"
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
context "after close" do
|
109
|
+
let(:endpoint) {
|
110
|
+
lambda {|env|
|
111
|
+
env['rack.stream'].instance_eval do
|
112
|
+
after_close do
|
113
|
+
$after_close_called = true
|
114
|
+
end
|
115
|
+
end
|
116
|
+
[200, {}, []]
|
117
|
+
}
|
118
|
+
}
|
119
|
+
|
120
|
+
it "should allow cleanup" do
|
121
|
+
$after_close_called.should be_true
|
122
|
+
$after_close_called = nil
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'bundler/setup'
|
2
|
+
require 'rack'
|
3
|
+
require 'rack/stream'
|
4
|
+
require 'rack/test'
|
5
|
+
require 'rspec'
|
6
|
+
|
7
|
+
Dir[File.expand_path('../support/**/*.rb', __FILE__)].each {|f| require f}
|
8
|
+
|
9
|
+
# Used by tests to untangle evented code, but not required for use w/ lib
|
10
|
+
require 'fiber'
|
11
|
+
require 'timeout'
|
12
|
+
|
13
|
+
# TODO: swap this with em-spec or something else
|
14
|
+
# Patch rspec to run examples in a reactor
|
15
|
+
# based on em-rspec, but with synchrony pattern and does not auto stop the reactor
|
16
|
+
RSpec::Core::Example.class_eval do
|
17
|
+
alias ignorant_run run
|
18
|
+
|
19
|
+
def run(example_group_instance, reporter)
|
20
|
+
EM.run do
|
21
|
+
Fiber.new do
|
22
|
+
EM.add_timer(2) {
|
23
|
+
raise Timeout::Error.new("aborting test due to timeout")
|
24
|
+
EM.stop
|
25
|
+
}
|
26
|
+
@ignorant_success = ignorant_run example_group_instance, reporter
|
27
|
+
end.resume
|
28
|
+
end
|
29
|
+
@ignorant_success
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
RSpec.configure do |c|
|
34
|
+
c.include Rack::Test::Methods
|
35
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Support
|
2
|
+
class MockServer
|
3
|
+
class Callback
|
4
|
+
attr_reader :status, :headers, :body
|
5
|
+
|
6
|
+
def initialize(&blk)
|
7
|
+
@succeed_callback = blk
|
8
|
+
end
|
9
|
+
|
10
|
+
def call(args)
|
11
|
+
@status, @headers, deferred_body = args
|
12
|
+
@body = []
|
13
|
+
deferred_body.each do |s|
|
14
|
+
@body << s
|
15
|
+
end
|
16
|
+
deferred_body.callback {@succeed_callback.call}
|
17
|
+
deferred_body.callback {EM.stop}
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def initialize(app)
|
22
|
+
@app = app
|
23
|
+
end
|
24
|
+
|
25
|
+
def call(env)
|
26
|
+
f = Fiber.current
|
27
|
+
callback = Callback.new do
|
28
|
+
f.resume [callback.status, callback.headers, callback.body]
|
29
|
+
end
|
30
|
+
env['async.callback'] = callback
|
31
|
+
@app.call(env)
|
32
|
+
Fiber.yield # wait until deferred body is succeeded
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
metadata
ADDED
@@ -0,0 +1,126 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rack-stream
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.2
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Jerry Cheung
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-05-14 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rack
|
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: eventmachine
|
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: faye-websocket
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :runtime
|
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
|
+
description: Rack middleware for building multi-protocol streaming rack endpoints
|
63
|
+
email:
|
64
|
+
- jerry@intridea.com
|
65
|
+
executables: []
|
66
|
+
extensions: []
|
67
|
+
extra_rdoc_files: []
|
68
|
+
files:
|
69
|
+
- .gitignore
|
70
|
+
- .rspec
|
71
|
+
- .travis.yml
|
72
|
+
- .yardopts
|
73
|
+
- Gemfile
|
74
|
+
- Guardfile
|
75
|
+
- LICENSE
|
76
|
+
- README.md
|
77
|
+
- Rakefile
|
78
|
+
- examples/basic.ru
|
79
|
+
- examples/loop.ru
|
80
|
+
- examples/no_stream.ru
|
81
|
+
- examples/websocket_client.rb
|
82
|
+
- lib/rack/stream.rb
|
83
|
+
- lib/rack/stream/app.rb
|
84
|
+
- lib/rack/stream/deferrable_body.rb
|
85
|
+
- lib/rack/stream/dsl.rb
|
86
|
+
- lib/rack/stream/handlers.rb
|
87
|
+
- rack-stream.gemspec
|
88
|
+
- spec/integration/server_spec.rb
|
89
|
+
- spec/integration/sinatra.ru
|
90
|
+
- spec/integration/views/index.erb
|
91
|
+
- spec/lib/rack/stream_spec.rb
|
92
|
+
- spec/spec_helper.rb
|
93
|
+
- spec/support/mock_server.rb
|
94
|
+
homepage: https://github.com/jch/rack-stream
|
95
|
+
licenses:
|
96
|
+
- BSD
|
97
|
+
post_install_message:
|
98
|
+
rdoc_options: []
|
99
|
+
require_paths:
|
100
|
+
- lib
|
101
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
102
|
+
none: false
|
103
|
+
requirements:
|
104
|
+
- - ! '>='
|
105
|
+
- !ruby/object:Gem::Version
|
106
|
+
version: '0'
|
107
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
108
|
+
none: false
|
109
|
+
requirements:
|
110
|
+
- - ! '>='
|
111
|
+
- !ruby/object:Gem::Version
|
112
|
+
version: '0'
|
113
|
+
requirements: []
|
114
|
+
rubyforge_project: rack-stream
|
115
|
+
rubygems_version: 1.8.24
|
116
|
+
signing_key:
|
117
|
+
specification_version: 3
|
118
|
+
summary: Rack middleware for building multi-protocol streaming rack endpoints
|
119
|
+
test_files:
|
120
|
+
- spec/integration/server_spec.rb
|
121
|
+
- spec/integration/sinatra.ru
|
122
|
+
- spec/integration/views/index.erb
|
123
|
+
- spec/lib/rack/stream_spec.rb
|
124
|
+
- spec/spec_helper.rb
|
125
|
+
- spec/support/mock_server.rb
|
126
|
+
has_rdoc:
|