rack-stream 0.0.2 → 0.0.3
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 +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
|
-
}
|