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 CHANGED
@@ -1,3 +1,4 @@
1
1
  .yardoc
2
2
  doc
3
3
  Gemfile.lock
4
+ pkg
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
 
@@ -1,4 +1,5 @@
1
1
  require 'eventmachine'
2
+ require 'em-synchrony'
2
3
  require 'faye/websocket'
3
4
 
4
5
  require 'rack/stream/handlers'
@@ -22,24 +22,7 @@ module Rack
22
22
  end
23
23
 
24
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
- }
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! #(server)
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
- modified = @callbacks["before_#{name}".to_sym].inject(args) do |memo, cb|
127
- [cb.call(*memo)]
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
- succeed if !flush
33
- succeed if flush && empty?
34
- schedule_dequeue if flush && !empty?
32
+ if !flush || empty?
33
+ succeed
34
+ else
35
+ schedule_dequeue
36
+ close!(flush)
37
+ end
35
38
  }
36
39
  end
37
40
 
@@ -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']
@@ -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 = 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
- raise NotImplementedError
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
- @body.chunk(*chunks.map {|c| encode_chunk(c)})
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!(flush = true)
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 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)
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
- class EventSource < WebSocket
84
- def chunk(*chunks)
85
- chunks.each {|c| @es.send(c)}
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 close!(flush = true)
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
@@ -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.2"
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 ws(m) {
16
- $('<li>').text(m).appendTo('#ws');
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 es(m) {
20
- $('<li>').text(m).appendTo('#es');
21
- }
20
+ function ws(m) {log('ws', m)}
21
+ function es(m) {log('es', m)}
22
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');}
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
- source = new EventSource('http://localhost:8888/');
30
- source.addEventListener('message', function(e) {
31
- es(e.data);
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.2
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-14 00:00:00.000000000 Z
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
@@ -1,7 +0,0 @@
1
- require 'rack/stream'
2
-
3
- use Rack::Stream
4
-
5
- run lambda {|env|
6
- [200, {'Content-Type' => 'text/plain'}, ['hello', ' ', 'world']]
7
- }
@@ -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
- }
@@ -1,4 +0,0 @@
1
- use Rack::Chunked
2
- run lambda {|env|
3
- [200, {'Content-Type' => 'text/plain'}, ['hello', 'world']]
4
- }
@@ -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
- }