async-rack 0.4.0.b
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +128 -0
- data/lib/async-rack.rb +1 -0
- data/lib/async_rack.rb +46 -0
- data/lib/async_rack/async_callback.rb +106 -0
- data/lib/async_rack/chunked.rb +7 -0
- data/lib/async_rack/commonlogger.rb +17 -0
- data/lib/async_rack/conditionalget.rb +7 -0
- data/lib/async_rack/content_length.rb +7 -0
- data/lib/async_rack/content_type.rb +7 -0
- data/lib/async_rack/deflater.rb +7 -0
- data/lib/async_rack/etag.rb +7 -0
- data/lib/async_rack/head.rb +7 -0
- data/lib/async_rack/lock.rb +9 -0
- data/lib/async_rack/logger.rb +21 -0
- data/lib/async_rack/runtime.rb +17 -0
- data/lib/async_rack/sendfile.rb +7 -0
- data/lib/async_rack/session/cookie.rb +11 -0
- data/lib/async_rack/session/memcache.rb +11 -0
- data/lib/async_rack/session/pool.rb +11 -0
- data/lib/async_rack/showstatus.rb +7 -0
- data/spec/async_rack/async_callback_spec.rb +55 -0
- data/spec/spec_helper.rb +2 -0
- metadata +113 -0
data/README.md
ADDED
@@ -0,0 +1,128 @@
|
|
1
|
+
# AsyncRack
|
2
|
+
|
3
|
+
So, have you been amazed by thin's `async.callback`? If not, [go](http://macournoyer.com/blog/2009/06/04/pusher-and-async-with-thin/) [check](http://github.com/raggi/async_sinatra) [it](http://github.com/raggi/thin/blob/async_for_rack/example/async_app.ru) [out](http://m.onkey.org/2010/1/7/introducing-cramp). Come back here when you start missing your middleware.
|
4
|
+
|
5
|
+
So what is the issue with Rack and `async.callback`? Currently there are two ways of triggering a async responds. The first is to `throw :async`, the latter to return a status code of -1 (even though thin and ebb do disagree on that). Opposed to what others say, I would recommend using `throw`, as it simply skips middleware not able to handle `:async`. Also, it works on all servers supporting `async.callback` – thin, ebb, rainbows! and zbatery – about the same and copes better with middleware that is unable to handle an async respond.
|
6
|
+
|
7
|
+
That's the issue with async.callback: Most middleware is not aware of it. Let's say you got an app somewhat like that:
|
8
|
+
|
9
|
+
class Farnsworth
|
10
|
+
def when_there_is_good_news
|
11
|
+
Thread.new do # Well, actually, you want to hook into your event loop instead, I guess.
|
12
|
+
wait_for_good_news
|
13
|
+
yield
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def call(env)
|
18
|
+
when_there_is_good_news do
|
19
|
+
env["async.callback"].call [200, {'Content-Type' => 'text/plain'}, ['Good news, everyone!']]
|
20
|
+
end
|
21
|
+
throw :async
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
Ok, now, since this app could end up on Reddit, you better prepare yourself for some heavy traffic. Say, you want to use the `Rack::Deflate` middleware, so you set it up in your config.ru and add the link to reddit yourself. The next day you get a call from your server admin. Why don't you at least compress your http response? Well what happened? The problem is, that by sending your response via `env["async.callback"].call` you talk directly to your web server (i.e. thin), bypassing all potential middleware.
|
26
|
+
|
27
|
+
Well, how do you avoid that? Simple: By just using middleware that plays well with `async.callback`. However, most middleware does not play well with it. In fact, most middleware that ships with rack does not play well with it. That's what I wrote this little library for. If you load `async-rack` it modifies all middleware that ships with rack, so it will work just fine with you throwing around your :async.
|
28
|
+
|
29
|
+
How does that work? Simple, whenever necessary, `async-rack` will replace `async.callback` with an appropriate proc object, so it has the chance to do it's response modifications whenever you feel like answering the http request.
|
30
|
+
|
31
|
+
Note: This library only 'fixes' the middleware that ships with rack, not other rack middleware. However, you can use the included helper classes to easily make other libraries handle `async.callback`.
|
32
|
+
|
33
|
+
## How to make a middleware async-proof?
|
34
|
+
There are three types of middleware:
|
35
|
+
|
36
|
+
### Middleware doing stuff before handing on the request
|
37
|
+
Example: `Rack:::MethodOverride`
|
38
|
+
|
39
|
+
Such middleware already works fine with `async.callback`. Also, from our perspective, middleware either creating a own response and not calling your app at all, or calling your app without modifying neither request nor response falls into this category, too.
|
40
|
+
|
41
|
+
Such middleware can easily be identified by having `@app.call(env)` or something similar as last line or always prefixed with a `return` inside the `call` method.
|
42
|
+
|
43
|
+
### Middleware doing stuff after handing on the request
|
44
|
+
Example: `Rack:::ETag`
|
45
|
+
|
46
|
+
Here it is a bit tricky. Essentially what you want is running `#call` again on an `async.callback` but replace `@app.call(env)` with the parameter passed to `async.callback`. Well, apparently this is the most common case inside rack, so I created a mixin for that:
|
47
|
+
|
48
|
+
# Ok, Rack::FancyStuff does currently not work with async responses
|
49
|
+
require 'rack/fancy_stuff'
|
50
|
+
|
51
|
+
class FixedFancyStuff < AsyncRack::AsyncCallback(:FancyStuff)
|
52
|
+
include AsyncRack::AsyncCallback::SimpleWrapper
|
53
|
+
end
|
54
|
+
|
55
|
+
See below to get an idea what actually happens here.
|
56
|
+
|
57
|
+
### Middleware meddling with both your request and your response
|
58
|
+
Example: `Rack::Runtime`
|
59
|
+
|
60
|
+
# Let's assume there is some not so async middleware.
|
61
|
+
module Rack
|
62
|
+
class FancyStuff
|
63
|
+
def initialize(app)
|
64
|
+
@app = app
|
65
|
+
end
|
66
|
+
|
67
|
+
def call(env)
|
68
|
+
prepare_fancy_stuff env
|
69
|
+
result = @app.call env
|
70
|
+
perform_fancy_stuff result
|
71
|
+
end
|
72
|
+
|
73
|
+
def prepare_fancy_stuff(env)
|
74
|
+
# ...
|
75
|
+
end
|
76
|
+
|
77
|
+
def perform_fancy_stuff(result)
|
78
|
+
# ...
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# What happens here is the following: We will subclass Rack::FancyStuff
|
84
|
+
# and then set Rack::FancyStuff = FixedFancyStuff. AsyncRack::AsyncCallback
|
85
|
+
# makes sure we don't screw that up.
|
86
|
+
class FixedFancyStuff < AsyncRack::AsyncCallback(:FancyStuff)
|
87
|
+
# this method will handle async.callback
|
88
|
+
def async_callback(result)
|
89
|
+
# pass it on to thin / ebb / other middleware
|
90
|
+
super perform_fancy_stuff(result)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
Rack::FancyStuff == FixedFancyStuff # => true
|
95
|
+
|
96
|
+
## Setup
|
97
|
+
In general: Place a `require 'async-rack'` before setting up any middleware or you will end up with the synchronous version!
|
98
|
+
|
99
|
+
Please keep in mind that it only "fixes" middleware that ships with rack. Read: It works very well with Sinatra. With Rails and Merb, not so much!
|
100
|
+
|
101
|
+
### With Rack
|
102
|
+
In your `config.ru`:
|
103
|
+
|
104
|
+
require 'async-rack'
|
105
|
+
require 'your-app'
|
106
|
+
|
107
|
+
use Rack::SomeMiddleware
|
108
|
+
run YourApp
|
109
|
+
|
110
|
+
### With Sinatra
|
111
|
+
In your application file:
|
112
|
+
|
113
|
+
require 'async-rack'
|
114
|
+
require 'sinatra'
|
115
|
+
|
116
|
+
get '/' do
|
117
|
+
# do some async stuff here
|
118
|
+
end
|
119
|
+
|
120
|
+
### With Rails 2.x
|
121
|
+
In your `config/environment.rb`, add inside the `Rails::Initializer.run` block:
|
122
|
+
|
123
|
+
config.gem 'async-rack'
|
124
|
+
|
125
|
+
### With Rails 3.x
|
126
|
+
In your `Gemfile`, add:
|
127
|
+
|
128
|
+
gem 'async-rack'
|
data/lib/async-rack.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "async_rack"
|
data/lib/async_rack.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
require "rack"
|
2
|
+
require "async_rack/async_callback"
|
3
|
+
|
4
|
+
module AsyncRack
|
5
|
+
module BaseMixin
|
6
|
+
::Rack.extend self
|
7
|
+
::Rack::Session.extend self
|
8
|
+
def autoload(class_name, path)
|
9
|
+
super unless autoload?(class_name) =~ /async_rack/
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
module ExtensionMixin
|
14
|
+
::AsyncRack.extend self
|
15
|
+
def autoload(class_name, path)
|
16
|
+
super
|
17
|
+
if Rack.autoload? class_name then Rack.autoload(class_name, path)
|
18
|
+
elsif Rack.const_defined? class_name then require path
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Wrapped rack middleware
|
24
|
+
autoload :Chunked, "async_rack/chunked"
|
25
|
+
autoload :CommonLogger, "async_rack/commonlogger"
|
26
|
+
autoload :ConditionalGet, "async_rack/conditionalget"
|
27
|
+
autoload :ContentLength, "async_rack/content_length"
|
28
|
+
autoload :ContentType, "async_rack/content_type"
|
29
|
+
autoload :Deflater, "async_rack/deflater"
|
30
|
+
autoload :ETag, "async_rack/etag"
|
31
|
+
autoload :Head, "async_rack/head"
|
32
|
+
autoload :Lock, "async_rack/lock"
|
33
|
+
autoload :Logger, "async_rack/logger"
|
34
|
+
# autoload :Recursive, "async_rack/recursive"
|
35
|
+
autoload :Runtime, "async_rack/runtime"
|
36
|
+
autoload :Sendfile, "async_rack/sendfile"
|
37
|
+
autoload :ShowStatus, "async_rack/showstatus"
|
38
|
+
|
39
|
+
module Session
|
40
|
+
extend ExtensionMixin
|
41
|
+
autoload :Cookie, "async_rack/session/cookie"
|
42
|
+
autoload :Pool, "async_rack/session/pool"
|
43
|
+
autoload :Memcache, "async_rack/session/memcache"
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
module AsyncRack
|
2
|
+
|
3
|
+
##
|
4
|
+
# @see AsyncRack::AsyncCallback
|
5
|
+
def self.AsyncCallback(name, namespace = Rack)
|
6
|
+
@wrapped ||= Hash.new { |h,k| h[k] = {} }
|
7
|
+
@wrapped[namespace][name.to_sym] ||= namespace.const_get(name).tap do |klass|
|
8
|
+
klass.extend AsyncCallback::InheritanceHook
|
9
|
+
klass.alias_subclass name, namespace
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
##
|
14
|
+
# Helps wrapping already existent middleware in a transparent manner.
|
15
|
+
#
|
16
|
+
# @example
|
17
|
+
# module Rack
|
18
|
+
# class FancyMiddleware
|
19
|
+
# end
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# module AsyncRack
|
23
|
+
# class FancyMiddleware < AsyncCallback(:FancyMiddleware)
|
24
|
+
# end
|
25
|
+
# end
|
26
|
+
#
|
27
|
+
# Rack::FancyMiddleware # => AsyncRack::FancyMiddleware
|
28
|
+
# AsyncRack::FancyMiddleware.ancestors # => [AsyncRack::AsyncCallback::Mixin, Rack::FancyMiddleware, ...]
|
29
|
+
module AsyncCallback
|
30
|
+
|
31
|
+
##
|
32
|
+
# Aliases a subclass on subclassing, but only once.
|
33
|
+
# If that name already is in use, it will be replaced.
|
34
|
+
#
|
35
|
+
# @example
|
36
|
+
# class Foo
|
37
|
+
# def self.bar
|
38
|
+
# 23
|
39
|
+
# end
|
40
|
+
# end
|
41
|
+
#
|
42
|
+
# Foo.extend AsyncRack::AsyncCallback::InheritanceHook
|
43
|
+
# Foo.alias_subclass :Baz
|
44
|
+
#
|
45
|
+
# class Bar < Foo
|
46
|
+
# def self.bar
|
47
|
+
# super + 19
|
48
|
+
# end
|
49
|
+
# end
|
50
|
+
#
|
51
|
+
# Baz.bar # => 42
|
52
|
+
module InheritanceHook
|
53
|
+
|
54
|
+
##
|
55
|
+
# @param [Symbol] name Name it will be aliased to
|
56
|
+
# @param [Class, Module] namespace The module the constant will be defined in
|
57
|
+
def alias_subclass(name, namespace = Object)
|
58
|
+
@alias_subclass = [name, namespace]
|
59
|
+
end
|
60
|
+
|
61
|
+
##
|
62
|
+
# @see InheritanceHook
|
63
|
+
def inherited(klass)
|
64
|
+
super
|
65
|
+
if @alias_subclass
|
66
|
+
name, namespace = @alias_subclass
|
67
|
+
@alias_subclass = nil
|
68
|
+
namespace.send :remove_const, name if namespace.const_defined? name
|
69
|
+
namespace.const_set name, klass
|
70
|
+
klass.send :include, AsyncRack::AsyncCallback::Mixin
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
module Mixin
|
76
|
+
def async_callback(result)
|
77
|
+
@async_callback.call result
|
78
|
+
end
|
79
|
+
|
80
|
+
def setup_async(env)
|
81
|
+
return false if @async_callback
|
82
|
+
@async_callback = env['async.callback']
|
83
|
+
env['async.callback'] = method :async_callback
|
84
|
+
@env = env
|
85
|
+
end
|
86
|
+
|
87
|
+
def call(env)
|
88
|
+
setup_async env
|
89
|
+
super
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
##
|
94
|
+
# A simple wrapper is useful if the first thing a middleware does is something like
|
95
|
+
# @app.call and then modifies the response.
|
96
|
+
#
|
97
|
+
# In that case you just have to include SimpleWrapper in your async wrapper class.
|
98
|
+
module SimpleWrapper
|
99
|
+
include AsyncRack::AsyncCallback::Mixin
|
100
|
+
def async_callback(result)
|
101
|
+
@app = proc { result }
|
102
|
+
super call(@env)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require "rack/commonlogger"
|
2
|
+
|
3
|
+
module AsyncRack
|
4
|
+
class CommonLogger < AsyncCallback(:CommonLogger)
|
5
|
+
def async_callback(result)
|
6
|
+
status, header, body = result
|
7
|
+
header = Rack::Utils::HeaderHash.new header
|
8
|
+
log env, status, header, @began_at
|
9
|
+
super [status, header, body]
|
10
|
+
end
|
11
|
+
|
12
|
+
def call(env)
|
13
|
+
@began_at = Time.now
|
14
|
+
super
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require "rack/logger"
|
2
|
+
|
3
|
+
module AsyncRack
|
4
|
+
class Logger < AsyncCallback(:Logger)
|
5
|
+
def async_callback(result)
|
6
|
+
@logger.close
|
7
|
+
super
|
8
|
+
end
|
9
|
+
|
10
|
+
def call(env)
|
11
|
+
@logger = ::Logger.new(env['rack.errors'])
|
12
|
+
@logger.level = @level
|
13
|
+
env['rack.logger'] = @logger
|
14
|
+
@app.call(env) # could throw :async
|
15
|
+
@logger.close
|
16
|
+
rescue Exception => error # does not get triggered by throwing :async (ensure does)
|
17
|
+
@logger.close
|
18
|
+
raise error
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require "rack/runtime"
|
2
|
+
|
3
|
+
module AsyncRack
|
4
|
+
class Runtime < AsyncCallback(:Runtime)
|
5
|
+
def async_callback(result)
|
6
|
+
status, headers, body =result
|
7
|
+
request_time = Time.now - start_time
|
8
|
+
headers[@header_name] = "%0.6f" % request_time if !headers.has_key?(@header_name)
|
9
|
+
[status, headers, body]
|
10
|
+
end
|
11
|
+
|
12
|
+
def call(env)
|
13
|
+
@start_time = Time.now
|
14
|
+
super
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require File.expand_path("../../spec_helper", __FILE__)
|
2
|
+
|
3
|
+
describe AsyncRack::AsyncCallback do
|
4
|
+
before do
|
5
|
+
@namespace = Module.new
|
6
|
+
@namespace.const_set :Foo, Class.new
|
7
|
+
end
|
8
|
+
|
9
|
+
it "wraps rack middleware by replacing it" do
|
10
|
+
non_async_middleware = @namespace::Foo
|
11
|
+
@namespace::Foo.should == non_async_middleware
|
12
|
+
async_middleware = Class.new AsyncRack::AsyncCallback(:Foo, @namespace)
|
13
|
+
@namespace::Foo.should_not == non_async_middleware
|
14
|
+
@namespace::Foo.should == async_middleware
|
15
|
+
AsyncRack::AsyncCallback(:Foo, @namespace).should == non_async_middleware
|
16
|
+
end
|
17
|
+
|
18
|
+
describe :InheritanceHook do
|
19
|
+
it "alows aliasing subclasses automatically" do
|
20
|
+
@namespace::Foo.extend AsyncRack::AsyncCallback::InheritanceHook
|
21
|
+
@namespace::Foo.alias_subclass :Bar, @namespace
|
22
|
+
subclass = Class.new(@namespace::Foo)
|
23
|
+
@namespace::Bar.should == subclass
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
describe :SimpleWrapper do
|
28
|
+
it "runs #call again on async callback, replacing app" do
|
29
|
+
klass = Class.new do
|
30
|
+
include AsyncRack::AsyncCallback::SimpleWrapper
|
31
|
+
attr_accessor :app, :env
|
32
|
+
def call(env)
|
33
|
+
setup_async env
|
34
|
+
@app.call(env) + 5
|
35
|
+
end
|
36
|
+
end
|
37
|
+
middleware = klass.new
|
38
|
+
middleware.app = proc { throw :async }
|
39
|
+
catch(:async) do
|
40
|
+
middleware.call "async.callback" => proc { |x| x + 10 }
|
41
|
+
raise "should not get here"
|
42
|
+
end
|
43
|
+
middleware.env["async.callback"].call(0).should == 15
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
describe :Mixin do
|
48
|
+
it "wrapps async.callback" do
|
49
|
+
@middleware = proc { |env| env['async.callback'].call [200, {'Content-Type' => 'text/plain'}, ['OK']] }
|
50
|
+
@middleware.extend AsyncRack::AsyncCallback::Mixin
|
51
|
+
@middleware.should_receive(:async_callback)
|
52
|
+
@middleware.call "async.callback" => proc { }
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,113 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: async-rack
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: true
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 4
|
8
|
+
- 0
|
9
|
+
- b
|
10
|
+
version: 0.4.0.b
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Konstantin Haase
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2010-03-09 00:00:00 +01:00
|
19
|
+
default_executable:
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
name: rack
|
23
|
+
prerelease: false
|
24
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
+
requirements:
|
26
|
+
- - ">="
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
segments:
|
29
|
+
- 1
|
30
|
+
- 1
|
31
|
+
- 0
|
32
|
+
version: 1.1.0
|
33
|
+
type: :runtime
|
34
|
+
version_requirements: *id001
|
35
|
+
- !ruby/object:Gem::Dependency
|
36
|
+
name: rspec
|
37
|
+
prerelease: false
|
38
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
39
|
+
requirements:
|
40
|
+
- - ">="
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
segments:
|
43
|
+
- 1
|
44
|
+
- 3
|
45
|
+
- 0
|
46
|
+
version: 1.3.0
|
47
|
+
type: :development
|
48
|
+
version_requirements: *id002
|
49
|
+
description: Makes middleware that ships with Rack bullet-proof for async responses.
|
50
|
+
email: konstantin.mailinglists@googlemail.com
|
51
|
+
executables: []
|
52
|
+
|
53
|
+
extensions: []
|
54
|
+
|
55
|
+
extra_rdoc_files: []
|
56
|
+
|
57
|
+
files:
|
58
|
+
- lib/async-rack.rb
|
59
|
+
- lib/async_rack/async_callback.rb
|
60
|
+
- lib/async_rack/chunked.rb
|
61
|
+
- lib/async_rack/commonlogger.rb
|
62
|
+
- lib/async_rack/conditionalget.rb
|
63
|
+
- lib/async_rack/content_length.rb
|
64
|
+
- lib/async_rack/content_type.rb
|
65
|
+
- lib/async_rack/deflater.rb
|
66
|
+
- lib/async_rack/etag.rb
|
67
|
+
- lib/async_rack/head.rb
|
68
|
+
- lib/async_rack/lock.rb
|
69
|
+
- lib/async_rack/logger.rb
|
70
|
+
- lib/async_rack/runtime.rb
|
71
|
+
- lib/async_rack/sendfile.rb
|
72
|
+
- lib/async_rack/session/cookie.rb
|
73
|
+
- lib/async_rack/session/memcache.rb
|
74
|
+
- lib/async_rack/session/pool.rb
|
75
|
+
- lib/async_rack/showstatus.rb
|
76
|
+
- lib/async_rack.rb
|
77
|
+
- spec/async_rack/async_callback_spec.rb
|
78
|
+
- spec/spec_helper.rb
|
79
|
+
- README.md
|
80
|
+
has_rdoc: yard
|
81
|
+
homepage: http://github.com/rkh/async-rack
|
82
|
+
licenses: []
|
83
|
+
|
84
|
+
post_install_message:
|
85
|
+
rdoc_options: []
|
86
|
+
|
87
|
+
require_paths:
|
88
|
+
- lib
|
89
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
90
|
+
requirements:
|
91
|
+
- - ">="
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
segments:
|
94
|
+
- 0
|
95
|
+
version: "0"
|
96
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
97
|
+
requirements:
|
98
|
+
- - ">"
|
99
|
+
- !ruby/object:Gem::Version
|
100
|
+
segments:
|
101
|
+
- 1
|
102
|
+
- 3
|
103
|
+
- 1
|
104
|
+
version: 1.3.1
|
105
|
+
requirements: []
|
106
|
+
|
107
|
+
rubyforge_project:
|
108
|
+
rubygems_version: 1.3.6
|
109
|
+
signing_key:
|
110
|
+
specification_version: 3
|
111
|
+
summary: Makes middleware that ships with Rack bullet-proof for async responses.
|
112
|
+
test_files: []
|
113
|
+
|