volt-sockjs 0.3.4.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENCE +19 -0
- data/README.textile +118 -0
- data/lib/meta-state.rb +151 -0
- data/lib/rack/sockjs.rb +173 -0
- data/lib/sockjs.rb +59 -0
- data/lib/sockjs/callbacks.rb +19 -0
- data/lib/sockjs/connection.rb +45 -0
- data/lib/sockjs/delayed-response-body.rb +99 -0
- data/lib/sockjs/duck-punch-rack-mount.rb +12 -0
- data/lib/sockjs/duck-punch-thin-response.rb +15 -0
- data/lib/sockjs/examples/protocol_conformance_test.rb +73 -0
- data/lib/sockjs/faye.rb +18 -0
- data/lib/sockjs/protocol.rb +97 -0
- data/lib/sockjs/servers/request.rb +137 -0
- data/lib/sockjs/servers/response.rb +170 -0
- data/lib/sockjs/session.rb +492 -0
- data/lib/sockjs/transport.rb +357 -0
- data/lib/sockjs/transports/eventsource.rb +30 -0
- data/lib/sockjs/transports/htmlfile.rb +73 -0
- data/lib/sockjs/transports/iframe.rb +68 -0
- data/lib/sockjs/transports/info.rb +48 -0
- data/lib/sockjs/transports/jsonp.rb +84 -0
- data/lib/sockjs/transports/websocket.rb +198 -0
- data/lib/sockjs/transports/welcome_screen.rb +17 -0
- data/lib/sockjs/transports/xhr.rb +75 -0
- data/lib/sockjs/version.rb +13 -0
- data/spec/sockjs/protocol_spec.rb +49 -0
- data/spec/sockjs/session_spec.rb +51 -0
- data/spec/sockjs/transport_spec.rb +73 -0
- data/spec/sockjs/transports/eventsource_spec.rb +56 -0
- data/spec/sockjs/transports/htmlfile_spec.rb +72 -0
- data/spec/sockjs/transports/iframe_spec.rb +66 -0
- data/spec/sockjs/transports/jsonp_spec.rb +252 -0
- data/spec/sockjs/transports/websocket_spec.rb +101 -0
- data/spec/sockjs/transports/welcome_screen_spec.rb +36 -0
- data/spec/sockjs/transports/xhr_spec.rb +314 -0
- data/spec/sockjs/version_spec.rb +18 -0
- data/spec/sockjs_spec.rb +8 -0
- data/spec/spec_helper.rb +121 -0
- data/spec/support/async-test.rb +42 -0
- metadata +154 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 3a6511da67d3f428defaa3d110be0be45654874e
|
4
|
+
data.tar.gz: 0e07f44fe5763660f1e8b59ef03e4102d9a07f85
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: cdbc681edbad7d6768d0d9644d9c2bc10fbc561ca866d38a7d2613dfe9b9f837f43447edab928c5c65933be41458f776b0007b1096f2e80ccf798c84d0d84ecf
|
7
|
+
data.tar.gz: 0d3f2c67e6ff12324cb3f6d96542249413ee39dd1332590500e745026fdf8c61a8e1fdcd189d945d47f53580a9cf21e2d30b1678795d75ab30939c004a06315c
|
data/LICENCE
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright (C) 2011 VMware, Inc.
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
5
|
+
in the Software without restriction, including without limitation the rights
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
THE SOFTWARE.
|
data/README.textile
ADDED
@@ -0,0 +1,118 @@
|
|
1
|
+
h1. About
|
2
|
+
|
3
|
+
_*Disclaimer:* This library is still work in progress._
|
4
|
+
|
5
|
+
SockJS is WebSocket emulation library. It means that you use the WebSocket API, only instead of @WebSocket@ class you instantiate @SockJS@ class. I highly recommend to read "SockJS: WebSocket emulation":http://www.rabbitmq.com/blog/2011/09/13/sockjs-websocket-emulation on the RabbitMQ blog for more info.
|
6
|
+
|
7
|
+
h2. Prerequisites
|
8
|
+
|
9
|
+
Even though this library uses Rack interface, *Thin is required* as "it supports asynchronous callback":http://macournoyer.com/blog/2009/06/04/pusher-and-async-with-thin. For Websockets, we use "faye-websocket":http://blog.jcoglan.com/2011/11/28/announcing-faye-websocket-a-standards-compliant-websocket-library gem.
|
10
|
+
|
11
|
+
h2. The Client-Side Part
|
12
|
+
|
13
|
+
For the client-side part you have to use JS library "sockjs-client":http://sockjs.github.com/sockjs-client which provides WebSocket-like API. Here's an example:
|
14
|
+
|
15
|
+
<pre>
|
16
|
+
<script src="http://cdn.sockjs.org/sockjs-0.2.1.min.js"></script>
|
17
|
+
|
18
|
+
<script>
|
19
|
+
var sock = new SockJS("http://mydomain.com/my_prefix");
|
20
|
+
|
21
|
+
sock.onopen = function() {
|
22
|
+
console.log("open");
|
23
|
+
};
|
24
|
+
|
25
|
+
sock.onmessage = function(e) {
|
26
|
+
console.log("message", e.data);
|
27
|
+
};
|
28
|
+
|
29
|
+
sock.onclose = function() {
|
30
|
+
console.log("close");
|
31
|
+
};
|
32
|
+
</script>
|
33
|
+
</pre>
|
34
|
+
|
35
|
+
h2. The Server-Side Part
|
36
|
+
|
37
|
+
Now in order to have someone to talk to, we need to run a server. That's exactly what is sockjs-ruby good for:
|
38
|
+
|
39
|
+
<pre>
|
40
|
+
#!/usr/bin/env ruby
|
41
|
+
# encoding: utf-8
|
42
|
+
|
43
|
+
require "rack"
|
44
|
+
require "rack/sockjs"
|
45
|
+
require "eventmachine"
|
46
|
+
|
47
|
+
# Your custom app.
|
48
|
+
class MyHelloWorld
|
49
|
+
def call(env)
|
50
|
+
body = "This is the app, not SockJS."
|
51
|
+
headers = {
|
52
|
+
"Content-Type" => "text/plain; charset=UTF-8",
|
53
|
+
"Content-Length" => body.bytesize.to_s
|
54
|
+
}
|
55
|
+
|
56
|
+
[200, headers, [body]]
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
|
61
|
+
app = Rack::Builder.new do
|
62
|
+
# Run one SockJS app on /echo.
|
63
|
+
use SockJS, "/echo" do |connection|
|
64
|
+
connection.subscribe do |session, message|
|
65
|
+
session.send(message)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# ... and the other one on /close.
|
70
|
+
use SockJS, "/close" do |connection|
|
71
|
+
connection.session_open do |session|
|
72
|
+
session.close(3000, "Go away!")
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# This app will run on other URLs than /echo and /close,
|
77
|
+
# as these has already been assigned to SockJS.
|
78
|
+
run MyHelloWorld.new
|
79
|
+
end
|
80
|
+
|
81
|
+
|
82
|
+
EM.run do
|
83
|
+
thin = Rack::Handler.get("thin")
|
84
|
+
thin.run(app.to_app, Port: 8081)
|
85
|
+
end
|
86
|
+
</pre>
|
87
|
+
|
88
|
+
For more complex example check "examples/sockjs_apps_for_sockjs_protocol_tests.rb":https://github.com/sockjs/sockjs-ruby/blob/master/examples/sockjs_apps_for_sockjs_protocol_tests.rb
|
89
|
+
|
90
|
+
|
91
|
+
h2. SockJS Family
|
92
|
+
|
93
|
+
* "SockJS-client":https://github.com/sockjs/sockjs-client JavaScript client library.
|
94
|
+
* "SockJS-node":https://github.com/sockjs/sockjs-node Node.js server.
|
95
|
+
* "SockJS-ruby":https://github.com/sockjs/sockjs-ruby Ruby server.
|
96
|
+
* "SockJS-protocol":https://github.com/sockjs/sockjs-protocol protocol tests and documentation.
|
97
|
+
* "SockJS-protocol spec":http://sockjs.github.com/sockjs-protocol/sockjs-protocol-0.2.1.html
|
98
|
+
|
99
|
+
h1. Development
|
100
|
+
|
101
|
+
Get "sockjs-protocol":https://github.com/sockjs/sockjs-protocol (installation information are in its README) and run @rake protocol_test@. Now you can run the tests against it, for instance:
|
102
|
+
|
103
|
+
<pre>
|
104
|
+
# Run all the tests.
|
105
|
+
./venv/bin/python sockjs-protocol-0.2.1.py
|
106
|
+
|
107
|
+
# Run all the tests defined in XhrStreaming.
|
108
|
+
./venv/bin/python sockjs-protocol-0.2.1.py XhrStreaming
|
109
|
+
|
110
|
+
# Run only XhrStreaming.test_transport test.
|
111
|
+
./venv/bin/python sockjs-protocol-0.2.1.py XhrStreaming.test_transport
|
112
|
+
</pre>
|
113
|
+
|
114
|
+
h1. Links
|
115
|
+
|
116
|
+
* "SockJS: WebSocket emulation":http://www.rabbitmq.com/blog/2011/09/13/sockjs-websocket-emulation
|
117
|
+
* "SockJS: web messaging ain't easy":http://www.rabbitmq.com/blog/2011/08/22/sockjs-web-messaging-aint-easy
|
118
|
+
* "PubSubHuddle Realtime Web talk":http://www.rabbitmq.com/blog/2011/09/26/pubsubhuddle-realtime-web-talk
|
data/lib/meta-state.rb
ADDED
@@ -0,0 +1,151 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
module MetaState
|
4
|
+
class Error < ::StandardError; end
|
5
|
+
class WrongStateError < Error; end
|
6
|
+
class InvalidStateError < Error; end
|
7
|
+
|
8
|
+
class Machine
|
9
|
+
NON_MESSAGES = [:on_exit, :on_enter]
|
10
|
+
|
11
|
+
class << self
|
12
|
+
def add_state(state)
|
13
|
+
@default_state ||= state
|
14
|
+
states
|
15
|
+
@states[state] = true
|
16
|
+
name = state.name.sub(/.*::/,'').downcase
|
17
|
+
state_names
|
18
|
+
@state_names[name] = state
|
19
|
+
@state_names[name.to_sym] = state
|
20
|
+
include state
|
21
|
+
@void_state_module = nil
|
22
|
+
end
|
23
|
+
|
24
|
+
def state(name, &block)
|
25
|
+
mod = Module.new(&block)
|
26
|
+
const_set(name, mod)
|
27
|
+
add_state(mod)
|
28
|
+
end
|
29
|
+
|
30
|
+
def default_state
|
31
|
+
@default_state || superclass.default_state
|
32
|
+
end
|
33
|
+
|
34
|
+
def void_state_module
|
35
|
+
if @void_state_module.nil?
|
36
|
+
build_void_state
|
37
|
+
end
|
38
|
+
@void_state_module
|
39
|
+
end
|
40
|
+
|
41
|
+
#Explicitly set the default (i.e. initial state) for an FSM
|
42
|
+
#Normally, this defaults to the first state defined, but some folks like
|
43
|
+
#to be explicit
|
44
|
+
def default_state=(state)
|
45
|
+
@default_state = state
|
46
|
+
end
|
47
|
+
|
48
|
+
def state_names
|
49
|
+
@state_names ||= {}
|
50
|
+
if Machine > superclass
|
51
|
+
superclass.state_names.merge(@state_names)
|
52
|
+
else
|
53
|
+
@state_names
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def states
|
58
|
+
@states ||= {}
|
59
|
+
if Machine > superclass
|
60
|
+
superclass.states.merge(@states)
|
61
|
+
else
|
62
|
+
@states
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def build_void_state
|
67
|
+
methods = (self.states.keys.map do |state|
|
68
|
+
state.instance_methods
|
69
|
+
end.flatten + NON_MESSAGES).uniq
|
70
|
+
|
71
|
+
@void_state_module = Module.new do
|
72
|
+
methods.each do |method|
|
73
|
+
if NON_MESSAGES.include?(method)
|
74
|
+
define_method(method){|*args| }
|
75
|
+
else
|
76
|
+
define_method(method) do |*args|
|
77
|
+
raise WrongStateError, "Message #{method} received in state #{current_state}"
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
include @void_state_module
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
#Explicitly put an FSM into a particular state. Simultaneously enters a
|
88
|
+
#state of sin. Use sparingly if at all.
|
89
|
+
def state=(state)
|
90
|
+
mod = state_module(state)
|
91
|
+
assign_state(mod)
|
92
|
+
end
|
93
|
+
|
94
|
+
def debug_with(&block)
|
95
|
+
@debug_block = block
|
96
|
+
end
|
97
|
+
|
98
|
+
attr_reader :current_state
|
99
|
+
def initialize
|
100
|
+
@debug_block = nil
|
101
|
+
assign_state(self.class.default_state)
|
102
|
+
end
|
103
|
+
|
104
|
+
protected
|
105
|
+
|
106
|
+
def debug
|
107
|
+
return if @debug_block.nil?
|
108
|
+
message = yield
|
109
|
+
@debug_block[message]
|
110
|
+
end
|
111
|
+
|
112
|
+
def assign_state(mod)
|
113
|
+
force_extend(self.class.void_state_module)
|
114
|
+
force_extend(mod)
|
115
|
+
@current_state = mod
|
116
|
+
end
|
117
|
+
|
118
|
+
def force_extend(mod)
|
119
|
+
mod.instance_methods.each do |method_name|
|
120
|
+
define_singleton_method(method_name, mod.instance_method(method_name))
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def state_module(state)
|
125
|
+
unless state.is_a? Module
|
126
|
+
state = self.class.state_names[state] unless state.is_a? Module
|
127
|
+
end
|
128
|
+
raise InvalidStateError unless self.class.states[state]
|
129
|
+
return state
|
130
|
+
end
|
131
|
+
|
132
|
+
def transition_to(state)
|
133
|
+
target_state = state_module(state)
|
134
|
+
return true if target_state == current_state
|
135
|
+
source_state = current_state
|
136
|
+
|
137
|
+
debug{ "Transitioning from #{source_state.inspect} to #{target_state.inspect}" }
|
138
|
+
|
139
|
+
on_exit
|
140
|
+
|
141
|
+
warn "State changed after on_exit method. Became: #{current_state.inspect}" unless source_state == current_state
|
142
|
+
|
143
|
+
assign_state(target_state)
|
144
|
+
|
145
|
+
on_enter
|
146
|
+
|
147
|
+
warn "State changed after on_enter method. Became: #{current_state.inspect}" unless target_state == current_state
|
148
|
+
end
|
149
|
+
|
150
|
+
end
|
151
|
+
end
|
data/lib/rack/sockjs.rb
ADDED
@@ -0,0 +1,173 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require "sockjs"
|
3
|
+
require 'sockjs/version'
|
4
|
+
require "sockjs/transport"
|
5
|
+
require "sockjs/servers/request"
|
6
|
+
require "sockjs/servers/response"
|
7
|
+
|
8
|
+
require 'rack/mount'
|
9
|
+
|
10
|
+
require 'sockjs/duck-punch-rack-mount'
|
11
|
+
require 'sockjs/duck-punch-thin-response'
|
12
|
+
|
13
|
+
# Transports.
|
14
|
+
require "sockjs/transports/info"
|
15
|
+
require "sockjs/transports/eventsource"
|
16
|
+
require "sockjs/transports/htmlfile"
|
17
|
+
require "sockjs/transports/iframe"
|
18
|
+
require "sockjs/transports/jsonp"
|
19
|
+
require "sockjs/transports/websocket"
|
20
|
+
require "sockjs/transports/welcome_screen"
|
21
|
+
require "sockjs/transports/xhr"
|
22
|
+
|
23
|
+
# This is a Rack middleware for SockJS.
|
24
|
+
#
|
25
|
+
#@example
|
26
|
+
#
|
27
|
+
# require 'rack/sockjs'
|
28
|
+
#
|
29
|
+
# map "/echo", Rack::SockJS.new do |connection|
|
30
|
+
# connection.subscribe do |session, message|
|
31
|
+
# session.send(message)
|
32
|
+
# end
|
33
|
+
# end
|
34
|
+
#
|
35
|
+
# run MyApp
|
36
|
+
#
|
37
|
+
#
|
38
|
+
# #or
|
39
|
+
#
|
40
|
+
# run Rack::SockJS.new do |connection|
|
41
|
+
# connection.session_open do |session|
|
42
|
+
# session.close(3000, "Go away!")
|
43
|
+
# end
|
44
|
+
# end
|
45
|
+
|
46
|
+
module Rack
|
47
|
+
class SockJS
|
48
|
+
SERVER_SESSION_REGEXP = %r{/([^/]*)/([^/]*)}
|
49
|
+
DEFAULT_OPTIONS = {
|
50
|
+
:sockjs_url => "http://cdn.sockjs.org/sockjs-#{::SockJS::PROTOCOL_VERSION_STRING}.min.js"
|
51
|
+
}
|
52
|
+
|
53
|
+
|
54
|
+
class DebugRequest
|
55
|
+
def initialize(app)
|
56
|
+
@app = app
|
57
|
+
end
|
58
|
+
|
59
|
+
def call(env)
|
60
|
+
request = ::SockJS::Request.new(env)
|
61
|
+
headers = request.headers.select { |key, value| not %w{version host accept-encoding}.include?(key.to_s) }
|
62
|
+
::SockJS.puts "\n~ \e[31m#{request.http_method} \e[32m#{request.path_info.inspect}#{" " + headers.inspect unless headers.empty?} \e[0m(\e[34m#{@prefix} app\e[0m)"
|
63
|
+
headers = headers.map { |key, value| "-H '#{key}: #{value}'" }.join(" ")
|
64
|
+
::SockJS.puts "\e[90mcurl -X #{request.http_method} http://localhost:8081#{request.path_info} #{headers}\e[0m"
|
65
|
+
|
66
|
+
result = @app.call(env)
|
67
|
+
ensure
|
68
|
+
::SockJS.debug "Rack response: " + result.inspect
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
class MissingHandler
|
73
|
+
def initialize(options)
|
74
|
+
end
|
75
|
+
|
76
|
+
def call(env)
|
77
|
+
prefix = env["PATH_INFO"]
|
78
|
+
method = env["REQUEST_METHOD"]
|
79
|
+
body = <<-HTML
|
80
|
+
<!DOCTYPE html>
|
81
|
+
<html>
|
82
|
+
<body>
|
83
|
+
<h1>Handler Not Found</h1>
|
84
|
+
<ul>
|
85
|
+
<li>Prefix: #{prefix.inspect}</li>
|
86
|
+
<li>Method: #{method.inspect}</li>
|
87
|
+
</ul>
|
88
|
+
</body>
|
89
|
+
</html>
|
90
|
+
HTML
|
91
|
+
::SockJS.debug "Handler not found!"
|
92
|
+
[404, {"Content-Type" => "text/html; charset=UTF-8", "Content-Length" => body.bytesize.to_s}, [body]]
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
class ResetScriptName
|
97
|
+
def initialize(app)
|
98
|
+
@app = app
|
99
|
+
end
|
100
|
+
|
101
|
+
def call(env)
|
102
|
+
env["PREVIOUS_SCRIPT_NAME"], env["SCRIPT_NAME"] = env["SCRIPT_NAME"], ''
|
103
|
+
result = @app.call(env)
|
104
|
+
ensure
|
105
|
+
env["SCRIPT_NAME"] = env["PREVIOUS_SCRIPT_NAME"]
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
class RenderErrors
|
110
|
+
def initialize(app)
|
111
|
+
@app = app
|
112
|
+
end
|
113
|
+
|
114
|
+
def call(env)
|
115
|
+
return @app.call(env)
|
116
|
+
rescue => err
|
117
|
+
if err.respond_to? :to_html
|
118
|
+
err.to_html
|
119
|
+
else
|
120
|
+
raise
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
class ExtractServerAndSession
|
126
|
+
def initialize(app)
|
127
|
+
@app = app
|
128
|
+
end
|
129
|
+
|
130
|
+
def call(env)
|
131
|
+
match = SERVER_SESSION_REGEXP.match(env["SCRIPT_NAME"])
|
132
|
+
env["sockjs.server-id"] = match[1]
|
133
|
+
env["sockjs.session-key"] = match[2]
|
134
|
+
old_script_name, old_path_name = env["SCRIPT_NAME"], env["PATH_INFO"]
|
135
|
+
env["SCRIPT_NAME"] = env["SCRIPT_NAME"] + match[0]
|
136
|
+
env["PATH_INFO"] = match.post_match
|
137
|
+
|
138
|
+
return @app.call(env)
|
139
|
+
ensure
|
140
|
+
env["SCRIPT_NAME"], env["PATH_INFO"] = old_script_name, old_path_name
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def initialize(session_class, options = nil)
|
145
|
+
#TODO refactor Connection to App
|
146
|
+
connection = ::SockJS::Connection.new(session_class, options)
|
147
|
+
|
148
|
+
options ||= {}
|
149
|
+
|
150
|
+
options = DEFAULT_OPTIONS.merge(options)
|
151
|
+
|
152
|
+
@routing = Rack::Mount::RouteSet.new do |set|
|
153
|
+
::SockJS::Endpoint.add_routes(set, connection, options)
|
154
|
+
|
155
|
+
set.add_route(MissingHandler.new(options), {}, {}, :missing)
|
156
|
+
end
|
157
|
+
|
158
|
+
routing = @routing
|
159
|
+
|
160
|
+
@app = Rack::Builder.new do
|
161
|
+
use Rack::SockJS::ResetScriptName
|
162
|
+
use DebugRequest
|
163
|
+
run routing
|
164
|
+
end.to_app
|
165
|
+
end
|
166
|
+
|
167
|
+
attr_reader :routing
|
168
|
+
|
169
|
+
def call(env)
|
170
|
+
@app.call(env)
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|