rack-stream 0.0.2
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 +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 [](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: 
         
     |