rack-stream 0.0.2 → 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -0
- data/README.md +8 -4
- data/lib/rack/stream.rb +1 -0
- data/lib/rack/stream/app.rb +32 -25
- data/lib/rack/stream/deferrable_body.rb +6 -3
- data/lib/rack/stream/dsl.rb +1 -1
- data/lib/rack/stream/handlers.rb +56 -29
- data/rack-stream.gemspec +3 -2
- data/spec/integration/views/index.erb +13 -15
- data/spec/lib/rack/stream_spec.rb +46 -0
- metadata +18 -6
- data/examples/basic.ru +0 -7
- data/examples/loop.ru +0 -20
- data/examples/no_stream.ru +0 -4
- data/examples/websocket_client.rb +0 -15
data/.gitignore
CHANGED
data/README.md
CHANGED
@@ -58,7 +58,8 @@ To run the example:
|
|
58
58
|
```
|
59
59
|
|
60
60
|
This same endpoint can be accessed via WebSockets or EventSource, see
|
61
|
-
'Multi-Protocol Support' below.
|
61
|
+
'Multi-Protocol Support' below. Full examples can be found in the `examples`
|
62
|
+
directory.
|
62
63
|
|
63
64
|
## Connection Lifecycle
|
64
65
|
|
@@ -82,7 +83,8 @@ Each state is described below.
|
|
82
83
|
When a request first comes in, rack-stream processes any downstream
|
83
84
|
rack apps and uses their status and headers for its response. Any
|
84
85
|
downstream response bodies are queued for streaming once the headers
|
85
|
-
and status have been sent.
|
86
|
+
and status have been sent. Any calls to `#chunk` before a connection
|
87
|
+
is opened is queued to be sent after a connection opens.
|
86
88
|
|
87
89
|
```ruby
|
88
90
|
use Rack::Stream
|
@@ -155,7 +157,7 @@ An instance enters the `:errored` state if an illegal action is
|
|
155
157
|
performed in one of the states. Legal actions for the different states
|
156
158
|
are:
|
157
159
|
|
158
|
-
* **new** - `#status=`, `#headers=`
|
160
|
+
* **new** - `#chunk`, `#status=`, `#headers=`
|
159
161
|
* **open** - `#chunk`, `#close`
|
160
162
|
|
161
163
|
All other actions are considered illegal. Manipulating headers after
|
@@ -264,13 +266,15 @@ source.addEventListener('message', function(e) {
|
|
264
266
|
* 1.9.3
|
265
267
|
|
266
268
|
If a runtime is not listed above, it may still work. It just means I
|
267
|
-
haven't tried it yet.
|
269
|
+
haven't tried it yet. The only app server I've tried running is Thin.
|
268
270
|
|
269
271
|
## Roadmap
|
270
272
|
|
271
273
|
* more protocols / custom protocols http://en.wikipedia.org/wiki/HTTP_Streaming
|
272
274
|
* integrate into [grape](http://github.com/intridea/grape)
|
273
275
|
* add sinatra example that serves page that uses JS to connect
|
276
|
+
* deployment guide
|
277
|
+
* better integration with rails
|
274
278
|
|
275
279
|
## Further Reading
|
276
280
|
|
data/lib/rack/stream.rb
CHANGED
data/lib/rack/stream/app.rb
CHANGED
@@ -22,24 +22,7 @@ module Rack
|
|
22
22
|
end
|
23
23
|
|
24
24
|
def call(env)
|
25
|
-
|
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
|
-
}
|
25
|
+
EM.next_tick {open!(env)}
|
43
26
|
ASYNC_RESPONSE
|
44
27
|
end
|
45
28
|
|
@@ -54,7 +37,7 @@ module Rack
|
|
54
37
|
end
|
55
38
|
|
56
39
|
def chunk(*chunks)
|
57
|
-
require_state :open
|
40
|
+
require_state :new, :open
|
58
41
|
run_callbacks(:chunk, chunks) {|mutated_chunks|
|
59
42
|
@handler.chunk(*mutated_chunks)
|
60
43
|
}
|
@@ -74,6 +57,11 @@ module Rack
|
|
74
57
|
}
|
75
58
|
end
|
76
59
|
|
60
|
+
# @return [String] name of the handler for this request
|
61
|
+
def stream_transport
|
62
|
+
@handler and @handler.class.name.split('::').last
|
63
|
+
end
|
64
|
+
|
77
65
|
def new?; @state == :new end
|
78
66
|
def open?; @state == :open end
|
79
67
|
def closed?; @state == :closed end
|
@@ -92,12 +80,29 @@ module Rack
|
|
92
80
|
# Transition state from :new to :open
|
93
81
|
#
|
94
82
|
# Freezes headers to prevent further modification
|
95
|
-
def open!
|
83
|
+
def open!(env)
|
84
|
+
@env = env
|
85
|
+
@env['rack.stream'] = self
|
96
86
|
raise UnsupportedServerError.new "missing async.callback. run within thin or rainbows" unless @env['async.callback']
|
87
|
+
|
97
88
|
run_callbacks(:open) {
|
89
|
+
@handler = Handlers.find(self)
|
90
|
+
@status, @headers, app_body = @app.call(@env)
|
91
|
+
|
92
|
+
app_body = if app_body.respond_to?(:body_parts)
|
93
|
+
app_body.body_parts
|
94
|
+
elsif app_body.respond_to?(:body)
|
95
|
+
app_body.body
|
96
|
+
else
|
97
|
+
app_body
|
98
|
+
end
|
99
|
+
|
100
|
+
chunk(*app_body) # chunk any downstream response bodies
|
101
|
+
after_open {close} if @callbacks[:after_open].empty?
|
102
|
+
|
103
|
+
@handler.open!
|
98
104
|
@state = :open
|
99
105
|
@headers.freeze
|
100
|
-
@handler.open!
|
101
106
|
}
|
102
107
|
end
|
103
108
|
|
@@ -123,11 +128,13 @@ module Rack
|
|
123
128
|
define_callbacks :close, :before, :after
|
124
129
|
|
125
130
|
def run_callbacks(name, *args)
|
126
|
-
|
127
|
-
[
|
131
|
+
EM.synchrony do
|
132
|
+
modified = @callbacks["before_#{name}".to_sym].inject(args) do |memo, cb|
|
133
|
+
[cb.call(*memo)]
|
134
|
+
end
|
135
|
+
yield(*modified) if block_given?
|
136
|
+
@callbacks["after_#{name}".to_sym].each {|cb| cb.call(*args)}
|
128
137
|
end
|
129
|
-
yield(*modified) if block_given?
|
130
|
-
@callbacks["after_#{name}".to_sym].each {|cb| cb.call(*args)}
|
131
138
|
rescue StateConstraintError => e
|
132
139
|
error!(e)
|
133
140
|
end
|
@@ -29,9 +29,12 @@ module Rack
|
|
29
29
|
|
30
30
|
def close!(flush = true)
|
31
31
|
EM.next_tick {
|
32
|
-
|
33
|
-
|
34
|
-
|
32
|
+
if !flush || empty?
|
33
|
+
succeed
|
34
|
+
else
|
35
|
+
schedule_dequeue
|
36
|
+
close!(flush)
|
37
|
+
end
|
35
38
|
}
|
36
39
|
end
|
37
40
|
|
data/lib/rack/stream/dsl.rb
CHANGED
@@ -6,7 +6,7 @@ module Rack
|
|
6
6
|
def self.included(base)
|
7
7
|
base.class_eval do
|
8
8
|
extend Forwardable
|
9
|
-
def_delegators :rack_stream, :after_open, :before_chunk, :chunk, :after_chunk, :before_close, :close, :after_close
|
9
|
+
def_delegators :rack_stream, :after_open, :before_chunk, :chunk, :after_chunk, :before_close, :close, :after_close, :stream_transport
|
10
10
|
|
11
11
|
def rack_stream
|
12
12
|
env['rack.stream']
|
data/lib/rack/stream/handlers.rb
CHANGED
@@ -1,6 +1,10 @@
|
|
1
1
|
module Rack
|
2
2
|
class Stream
|
3
|
+
# A Handler is responsible for opening and closing connections
|
4
|
+
# to stream content.
|
3
5
|
module Handlers
|
6
|
+
# @private
|
7
|
+
# TODO: allow registration of custom protocols
|
4
8
|
def find(app)
|
5
9
|
if Faye::WebSocket.websocket?(app.env)
|
6
10
|
WebSocket.new(app)
|
@@ -12,49 +16,68 @@ module Rack
|
|
12
16
|
end
|
13
17
|
module_function :find
|
14
18
|
|
19
|
+
# All handlers should inherit from `AbstractHandler`
|
15
20
|
class AbstractHandler
|
21
|
+
|
22
|
+
# @param app [Rack::Stream::App] reference to current request
|
16
23
|
def initialize(app)
|
17
|
-
@app
|
24
|
+
@app = app
|
25
|
+
@body = DeferrableBody.new
|
18
26
|
end
|
19
27
|
|
28
|
+
# Enqueue content to be streamed at a later time.
|
29
|
+
#
|
30
|
+
# Optionally override this method if you need to control
|
31
|
+
# the content at a protocol level.
|
20
32
|
def chunk(*chunks)
|
21
|
-
|
33
|
+
@body.chunk(*chunks)
|
22
34
|
end
|
23
35
|
|
36
|
+
# @private
|
24
37
|
def open!
|
38
|
+
open
|
39
|
+
end
|
40
|
+
|
41
|
+
# Implement `#open` to initiate a connection
|
42
|
+
def open
|
25
43
|
raise NotImplementedError
|
26
44
|
end
|
27
45
|
|
46
|
+
# @private
|
28
47
|
def close!(flush = true)
|
48
|
+
close
|
49
|
+
@body.close!(flush)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Implement `#close` for cleanup
|
53
|
+
# `#close` is called before the DeferrableBody is succeeded.
|
54
|
+
def close
|
29
55
|
raise NotImplementedError
|
30
56
|
end
|
31
57
|
end
|
32
58
|
|
59
|
+
# This Handler works under EventMachine aware Rack servers like Thin
|
60
|
+
# and Rainbows! It does chunked transfer encoding.
|
33
61
|
class Http < AbstractHandler
|
34
62
|
TERM = "\r\n".freeze
|
35
63
|
TAIL = "0#{TERM}#{TERM}".freeze
|
36
64
|
|
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
65
|
def chunk(*chunks)
|
45
|
-
|
66
|
+
super(*chunks.map {|c| encode_chunk(c)})
|
46
67
|
end
|
47
68
|
|
48
|
-
def open
|
69
|
+
def open
|
70
|
+
@app.headers['Transfer-Encoding'] = 'chunked'
|
71
|
+
@app.headers.delete('Content-Length')
|
49
72
|
@app.env['async.callback'].call [@app.status, @app.headers, @body]
|
50
73
|
end
|
51
74
|
|
52
|
-
def close
|
75
|
+
def close
|
53
76
|
@body.chunk(TAIL) # tail is special and already encoded
|
54
|
-
@body.close!(flush)
|
55
77
|
end
|
56
78
|
|
57
79
|
private
|
80
|
+
|
58
81
|
def encode_chunk(c)
|
59
82
|
return nil if c.nil?
|
60
83
|
|
@@ -65,32 +88,36 @@ module Rack
|
|
65
88
|
end
|
66
89
|
end
|
67
90
|
|
91
|
+
# This handler uses delegates WebSocket requests to faye-websocket
|
68
92
|
class WebSocket < AbstractHandler
|
69
|
-
def
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
def close!(flush = true)
|
75
|
-
@ws.close(@app.status)
|
93
|
+
def close
|
94
|
+
@body.callback {
|
95
|
+
@ws.close(@app.status)
|
96
|
+
}
|
76
97
|
end
|
77
98
|
|
78
|
-
def open
|
99
|
+
def open
|
79
100
|
@ws = Faye::WebSocket.new(@app.env)
|
101
|
+
@ws.onopen = lambda do |event|
|
102
|
+
@body.each {|c| @ws.send(c)}
|
103
|
+
end
|
80
104
|
end
|
81
105
|
end
|
82
106
|
|
83
|
-
|
84
|
-
|
85
|
-
|
107
|
+
# This handler uses delegates EventSource requests to faye-websocket
|
108
|
+
class EventSource < AbstractHandler
|
109
|
+
# TODO: browser initiates connection again, isn't closed
|
110
|
+
def close
|
111
|
+
@body.callback {
|
112
|
+
@es.close
|
113
|
+
}
|
86
114
|
end
|
87
115
|
|
88
|
-
def
|
89
|
-
@es.close
|
90
|
-
end
|
91
|
-
|
92
|
-
def open!
|
116
|
+
def open
|
93
117
|
@es = Faye::EventSource.new(@app.env)
|
118
|
+
@es.onopen = lambda do |event|
|
119
|
+
@body.each {|c| @es.send(c)}
|
120
|
+
end
|
94
121
|
end
|
95
122
|
end
|
96
123
|
end
|
data/rack-stream.gemspec
CHANGED
@@ -2,7 +2,7 @@ $:.push File.expand_path("../lib", __FILE__)
|
|
2
2
|
|
3
3
|
Gem::Specification.new do |s|
|
4
4
|
s.name = "rack-stream"
|
5
|
-
s.version = "0.0.
|
5
|
+
s.version = "0.0.3"
|
6
6
|
s.platform = Gem::Platform::RUBY
|
7
7
|
s.authors = ["Jerry Cheung"]
|
8
8
|
s.email = ["jerry@intridea.com"]
|
@@ -16,8 +16,9 @@ Gem::Specification.new do |s|
|
|
16
16
|
s.add_runtime_dependency 'rack'
|
17
17
|
s.add_runtime_dependency 'eventmachine'
|
18
18
|
s.add_runtime_dependency 'faye-websocket'
|
19
|
+
s.add_runtime_dependency 'em-synchrony'
|
19
20
|
|
20
|
-
s.files = `git ls-files`.split("\n")
|
21
|
+
s.files = `git ls-files`.split("\n").select {|f| f !~ /^example/}
|
21
22
|
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
22
23
|
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
23
24
|
s.require_paths = ["lib"]
|
@@ -8,28 +8,26 @@
|
|
8
8
|
<h2>EventSource</h2>
|
9
9
|
<ul id='es'></ul>
|
10
10
|
|
11
|
-
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
|
12
11
|
<script type='text/javascript'>
|
13
12
|
var socket, source;
|
14
13
|
|
15
|
-
function
|
16
|
-
|
14
|
+
function log(id, message) {
|
15
|
+
var li = document.createElement('li');
|
16
|
+
li.textContent = message;
|
17
|
+
document.getElementById(id).appendChild(li);
|
17
18
|
}
|
18
19
|
|
19
|
-
function
|
20
|
-
|
21
|
-
}
|
20
|
+
function ws(m) {log('ws', m)}
|
21
|
+
function es(m) {log('es', m)}
|
22
22
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
socket.onclose = function() {ws('socket closed');}
|
23
|
+
socket = new WebSocket("ws://localhost:8888/");
|
24
|
+
socket.onopen = function() {ws('socket opened');}
|
25
|
+
socket.onmessage = function(m) {ws(m.data);}
|
26
|
+
socket.onclose = function() {ws('socket closed');}
|
28
27
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
});
|
28
|
+
source = new EventSource('http://localhost:8888/');
|
29
|
+
source.addEventListener('message', function(e) {
|
30
|
+
es(e.data);
|
33
31
|
});
|
34
32
|
</script>
|
35
33
|
</body>
|
@@ -45,6 +45,42 @@ describe Rack::Stream do
|
|
45
45
|
end
|
46
46
|
end
|
47
47
|
|
48
|
+
context "queued content" do
|
49
|
+
let(:endpoint) {
|
50
|
+
lambda {|env|
|
51
|
+
env['rack.stream'].chunk "Chunky"
|
52
|
+
[200, {}, ['']]
|
53
|
+
}
|
54
|
+
}
|
55
|
+
|
56
|
+
it "should allow chunks to be queued outside of callbacks" do
|
57
|
+
last_response.body.should == "6\r\nChunky\r\n0\r\n\r\n"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
context "synchrony" do
|
62
|
+
let(:endpoint) {
|
63
|
+
lambda {|env|
|
64
|
+
f = Fiber.current
|
65
|
+
EM.next_tick do
|
66
|
+
f.resume [200, {}, ["Chunky"]]
|
67
|
+
end
|
68
|
+
|
69
|
+
env['rack.stream'].instance_eval do
|
70
|
+
after_open do
|
71
|
+
chunk "Monkey"
|
72
|
+
close
|
73
|
+
end
|
74
|
+
end
|
75
|
+
Fiber.yield
|
76
|
+
}
|
77
|
+
}
|
78
|
+
|
79
|
+
it "should wrap evaluation in a fiber" do
|
80
|
+
last_response.body.should == "6\r\nChunky\r\n6\r\nMonkey\r\n0\r\n\r\n"
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
48
84
|
context "basic streaming" do
|
49
85
|
let(:endpoint) {
|
50
86
|
lambda {|env|
|
@@ -122,4 +158,14 @@ describe Rack::Stream do
|
|
122
158
|
$after_close_called = nil
|
123
159
|
end
|
124
160
|
end
|
161
|
+
|
162
|
+
context "stream transport" do
|
163
|
+
let(:endpoint) {
|
164
|
+
lambda {|env| [200, {}, [env['rack.stream'].stream_transport]]}
|
165
|
+
}
|
166
|
+
|
167
|
+
it "should be http by default" do
|
168
|
+
last_response.body.should == "4\r\nHttp\r\n0\r\n\r\n"
|
169
|
+
end
|
170
|
+
end
|
125
171
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rack-stream
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.3
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-05-
|
12
|
+
date: 2012-05-22 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rack
|
@@ -59,6 +59,22 @@ dependencies:
|
|
59
59
|
- - ! '>='
|
60
60
|
- !ruby/object:Gem::Version
|
61
61
|
version: '0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: em-synchrony
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
type: :runtime
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
62
78
|
description: Rack middleware for building multi-protocol streaming rack endpoints
|
63
79
|
email:
|
64
80
|
- jerry@intridea.com
|
@@ -75,10 +91,6 @@ files:
|
|
75
91
|
- LICENSE
|
76
92
|
- README.md
|
77
93
|
- Rakefile
|
78
|
-
- examples/basic.ru
|
79
|
-
- examples/loop.ru
|
80
|
-
- examples/no_stream.ru
|
81
|
-
- examples/websocket_client.rb
|
82
94
|
- lib/rack/stream.rb
|
83
95
|
- lib/rack/stream/app.rb
|
84
96
|
- lib/rack/stream/deferrable_body.rb
|
data/examples/basic.ru
DELETED
data/examples/loop.ru
DELETED
@@ -1,20 +0,0 @@
|
|
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
|
-
}
|
data/examples/no_stream.ru
DELETED
@@ -1,15 +0,0 @@
|
|
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
|
-
}
|