File: design.md

package info (click to toggle)
ruby-protocol-http 0.23.12-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm, forky, sid, trixie
  • size: 472 kB
  • sloc: ruby: 2,794; makefile: 4
file content (167 lines) | stat: -rw-r--r-- 2,928 bytes parent folder | download
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)