async-rack 0.4.0.b
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/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
|
+
|