1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167
|
# Middleware Design
`Body::Writable` is a queue of String chunks.
## Request Response Model
~~~ruby
class Request
attr :verb
attr :target
attr :body
end
class Response
attr :status
attr :headers
attr :body
end
def call(request)
return response
end
def call(request)
return @app.call(request)
end
~~~
## Stream Model
~~~ruby
class Stream
attr :verb
attr :target
def respond(status, headers) = ...
attr_accessor :input
attr_accessor :output
end
class Response
def initialize(verb, target)
@input = Body::Writable.new
@output = Body::Writable.new
end
def request(verb, target)
# Create a request stream suitable for writing into the buffered response:
Stream.new(verb, target, @input, @output)
end
def write(...)
@input.write(...)
end
def read
@output.read
end
end
def call(stream)
# nothing. maybe error
end
def call(stream)
@app.call(stream)
end
~~~
# Client Design
## Request Response Model
~~~ruby
request = Request.new("GET", url)
response = call(request)
response.headers
response.read
~~~
## Stream Model
~~~ruby
response = Response.new
call(response.request("GET", url))
response.headers
response.read
~~~
## Differences
The request/response model has a symmetrical design which naturally uses the return value for the result of executing the request. The result encapsulates the behaviour of how to read the response status, headers and body. Because of that, streaming input and output becomes a function of the result object itself. As in:
~~~ruby
def call(request)
body = Body::Writable.new
Fiber.schedule do
while chunk = request.input.read
body.write(chunk.reverse)
end
end
return Response[200, [], body]
end
input = Body::Writable.new
response = call(... body ...)
input.write("Hello World")
input.close
response.read -> "dlroW olleH"
~~~
The streaming model does not have the same symmetry, and instead opts for a uni-directional flow of information.
~~~ruby
def call(stream)
Fiber.schedule do
while chunk = stream.read
stream.write(chunk.reverse)
end
end
end
input = Body::Writable.new
response = Response.new(...input...)
call(response.stream)
input.write("Hello World")
input.close
response.read -> "dlroW olleH"
~~~
The value of this uni-directional flow is that it is natural for the stream to be taken out of the scope imposed by the nested `call(request)` model. However, the user must explicitly close the stream, since it's no longer scoped to the client and/or server.
## Connection Upgrade
### HTTP/1
```
GET /path/to/websocket HTTP/1.1
connection: upgrade
upgrade: websocket
```
Request.new(GET, ..., protocol = websocket)
-> Response.new(101, ..., protocol = websocket)
```
101 Switching Protocols
upgrade: websocket
```
### HTTP/2
```
:method CONNECT
:path /path/to/websocket
:protocol websocket
```
Request.new(CONNECT, ..., protocol = websocket)
-> Response.new(200, ..., protocol = websocket)
|