protocol-http 0.33.0 → 0.34.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1bf929d8d011f9c79cf7b92bdf8f5eaefcd4faab288b555a7889f33a995daf2e
4
- data.tar.gz: 4de5349ae1e3cee6fb4857dbac1c78ef7e44249e1adc4526674803d0524af8f1
3
+ metadata.gz: cb1197435097557cab74b1b61b92fe0a15a938892dd07486ee9f9db06360decd
4
+ data.tar.gz: 78bcbbd73817b03ce63b55b2bbd4408afeaeaa127e3c19f9dfd5d6ddb120f05d
5
5
  SHA512:
6
- metadata.gz: b2214c233b4543e929f23ef59e8ffaed14f597c9762596ffd8afc8d9a1657fc6c50ace4dc1859a10033e2bba68e36d8f10a8486a056444676939d73352ed374f
7
- data.tar.gz: 27d9318755a9a2027e21d144a361287adf9cc6f2990bcae4ce93cbd9767c125e6811a1d19c20cf2da5ec67f729132271c9e1744c6ea1c82a88c3fff795e71502
6
+ metadata.gz: 34db4e5940f59773708692bc080706be457f9f41445b6858e01da474f0ba36aba31d8d8c2f3ccd329f2120f412d9c4bcc42c568c057db3e0cef816c83ab34db1
7
+ data.tar.gz: 23baab35d26b5b367fa556d4a302f81680b944fef07b2ca0a581fab7b36ac8320cb3c4a62cdc50f8cfd21efb408eee01e348c06b4c538a5cfbcf8cd6ce99b28f
checksums.yaml.gz.sig CHANGED
Binary file
@@ -56,6 +56,17 @@ module Protocol
56
56
  self
57
57
  end
58
58
 
59
+ # Ensure that future reads return nil, but allow for rewinding.
60
+ def close(error = nil)
61
+ @index = @chunks.length
62
+ end
63
+
64
+ def clear
65
+ @chunks.clear
66
+ @length = 0
67
+ @index = 0
68
+ end
69
+
59
70
  def length
60
71
  @length ||= @chunks.inject(0) {|sum, chunk| sum + chunk.bytesize}
61
72
  end
@@ -70,6 +81,8 @@ module Protocol
70
81
  end
71
82
 
72
83
  def read
84
+ return nil unless @chunks
85
+
73
86
  if chunk = @chunks[@index]
74
87
  @index += 1
75
88
 
@@ -81,18 +94,26 @@ module Protocol
81
94
  @chunks << chunk
82
95
  end
83
96
 
97
+ def close_write(error)
98
+ # Nothing to do.
99
+ end
100
+
84
101
  def rewindable?
85
- true
102
+ @chunks != nil
86
103
  end
87
104
 
88
105
  def rewind
106
+ return false unless @chunks
107
+
89
108
  @index = 0
90
109
 
91
110
  return true
92
111
  end
93
112
 
94
113
  def inspect
95
- "\#<#{self.class} #{@chunks.size} chunks, #{self.length} bytes>"
114
+ if @chunks
115
+ "\#<#{self.class} #{@chunks.size} chunks, #{self.length} bytes>"
116
+ end
96
117
  end
97
118
  end
98
119
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2019-2023, by Samuel Williams.
4
+ # Copyright, 2019-2024, by Samuel Williams.
5
5
 
6
6
  require_relative 'wrapper'
7
7
 
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2020-2023, by Samuel Williams.
4
+ # Copyright, 2020-2024, by Samuel Williams.
5
5
 
6
6
  require_relative 'wrapper'
7
7
 
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2019-2023, by Samuel Williams.
4
+ # Copyright, 2019-2024, by Samuel Williams.
5
5
 
6
6
  require_relative 'readable'
7
7
 
@@ -68,13 +68,15 @@ module Protocol
68
68
  end
69
69
  end
70
70
 
71
- def stream?
72
- true
73
- end
71
+ # def stream?
72
+ # true
73
+ # end
74
74
 
75
- def call(stream)
76
- IO.copy_stream(@file, stream, @remaining)
77
- end
75
+ # def call(stream)
76
+ # IO.copy_stream(@file, stream, @remaining)
77
+ # ensure
78
+ # stream.close
79
+ # end
78
80
 
79
81
  def join
80
82
  return "" if @remaining == 0
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2019-2023, by Samuel Williams.
4
+ # Copyright, 2019-2024, by Samuel Williams.
5
5
 
6
6
  require 'zlib'
7
7
 
@@ -17,7 +17,11 @@ module Protocol
17
17
  #
18
18
  # If you don't want to read from a stream, and instead want to close it immediately, you can call `close` on the body. If the body is already completely consumed, `close` will do nothing, but if there is still data to be read, it will cause the underlying stream to be reset (and possibly closed).
19
19
  class Readable
20
- # Close the stream immediately.
20
+ # Close the stream immediately. After invoking this method, the stream should be considered closed, and all internal resources should be released.
21
+ #
22
+ # If an error occured while handling the output, it can be passed as an argument. This may be propagated to the client, for example the client may be informed that the stream was not fully read correctly.
23
+ #
24
+ # Invoking `#read` after `#close` will return `nil`.
21
25
  def close(error = nil)
22
26
  end
23
27
 
@@ -68,13 +72,15 @@ module Protocol
68
72
  def each
69
73
  return to_enum unless block_given?
70
74
 
71
- while chunk = self.read
72
- yield chunk
75
+ begin
76
+ while chunk = self.read
77
+ yield chunk
78
+ end
79
+ rescue => error
80
+ raise
81
+ ensure
82
+ self.close(error)
73
83
  end
74
- rescue => error
75
- raise
76
- ensure
77
- self.close(error)
78
84
  end
79
85
 
80
86
  # Read all remaining chunks into a single binary string using `#each`.
@@ -98,12 +104,14 @@ module Protocol
98
104
  false
99
105
  end
100
106
 
101
- # Write the body to the given stream.
107
+ # Invoke the body with the given stream.
102
108
  #
103
- # In some cases, the stream may also be readable, such as when hijacking an HTTP/1 connection. In that case, it may be acceptable to read and write to the stream directly.
109
+ # The default implementation simply writes each chunk to the stream. If the body is not ready, it will be flushed after each chunk. Closes the stream when finished or if an error occurs.
104
110
  #
105
- # If the stream is not ready, it will be flushed after each chunk. Closes the stream when finished or if an error occurs.
111
+ # Write the body to the given stream.
106
112
  #
113
+ # @parameter stream [IO | Object] An `IO`-like object that responds to `#read`, `#write` and `#flush`.
114
+ # @returns [Boolean] Whether the ownership of the stream was transferred.
107
115
  def call(stream)
108
116
  self.each do |chunk|
109
117
  stream.write(chunk)
@@ -113,6 +121,8 @@ module Protocol
113
121
  stream.flush
114
122
  end
115
123
  end
124
+ ensure
125
+ stream.close
116
126
  end
117
127
 
118
128
  # Read all remaining chunks into a buffered body and close the underlying input.
@@ -21,6 +21,7 @@ module Protocol
21
21
 
22
22
  # Will hold remaining data in `#read`.
23
23
  @buffer = nil
24
+
24
25
  @closed = false
25
26
  @closed_read = false
26
27
  end
@@ -251,21 +252,22 @@ module Protocol
251
252
  end
252
253
 
253
254
  # Close the input body.
254
- def close_read
255
- if @input
255
+ def close_read(error = nil)
256
+ if input = @input
257
+ @input = nil
256
258
  @closed_read = true
257
259
  @buffer = nil
258
260
 
259
- @input&.close
260
- @input = nil
261
+ input.close(error)
261
262
  end
262
263
  end
263
264
 
264
265
  # Close the output body.
265
- def close_write
266
- if @output
267
- @output&.close
266
+ def close_write(error = nil)
267
+ if output = @output
268
268
  @output = nil
269
+
270
+ output.close_write(error)
269
271
  end
270
272
  end
271
273
 
@@ -1,9 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2022, by Samuel Williams.
4
+ # Copyright, 2019-2024, by Samuel Williams.
5
5
 
6
6
  require_relative 'readable'
7
+ require_relative 'writable'
8
+
7
9
  require_relative 'stream'
8
10
 
9
11
  module Protocol
@@ -14,98 +16,137 @@ module Protocol
14
16
  # In some cases, it's advantageous to directly read and write to the underlying stream if possible. For example, HTTP/1 upgrade requests, WebSockets, and similar. To handle that case, response bodies can implement `stream?` and return `true`. When `stream?` returns true, the body **should** be consumed by calling `call(stream)`. Server implementations may choose to always invoke `call(stream)` if it's efficient to do so. Bodies that don't support it will fall back to using `#each`.
15
17
  #
16
18
  # When invoking `call(stream)`, the stream can be read from and written to, and closed. However, the stream is only guaranteed to be open for the duration of the `call(stream)` call. Once the method returns, the stream **should** be closed by the server.
17
- class Streamable < Readable
18
- class Closed < StandardError
19
+ module Streamable
20
+ def self.new(*arguments)
21
+ if arguments.size == 1
22
+ DeferredBody.new(*arguments)
23
+ else
24
+ Body.new(*arguments)
25
+ end
26
+ end
27
+
28
+ def self.request(&block)
29
+ DeferredBody.new(block)
19
30
  end
20
31
 
21
- def initialize(block, input = nil)
22
- @block = block
23
- @input = input
24
- @output = nil
32
+ def self.response(request, &block)
33
+ Body.new(block, request.body)
25
34
  end
26
35
 
27
- # Closing a stream indicates we are no longer interested in reading from it.
28
- def close(error = nil)
29
- if @input
30
- @input.close
31
- @input = nil
36
+ class Output
37
+ def self.schedule(input, block)
38
+ self.new(input, block).tap(&:schedule)
39
+ end
40
+
41
+ def initialize(input, block)
42
+ @output = Writable.new
43
+ @stream = Stream.new(input, @output)
44
+ @block = block
45
+ end
46
+
47
+ def schedule
48
+ @fiber ||= Fiber.schedule do
49
+ @block.call(@stream)
50
+ end
32
51
  end
33
52
 
34
- if @output
35
- @output.close(error)
53
+ def read
54
+ @output.read
55
+ end
56
+
57
+ def close(error = nil)
58
+ @output.close_write(error)
36
59
  end
37
60
  end
38
61
 
39
- attr :block
62
+ # Raised when a streaming body is consumed more than once.
63
+ class ConsumedError < StandardError
64
+ end
40
65
 
41
- class Output
42
- def initialize(input, block)
43
- stream = Stream.new(input, self)
44
-
45
- @from = nil
46
-
47
- @fiber = Fiber.new do |from|
48
- @from = from
49
- block.call(stream)
50
- rescue Closed
51
- # Ignore.
52
- ensure
53
- @fiber = nil
54
-
55
- # No more chunks will be generated:
56
- if from = @from
57
- @from = nil
58
- from.transfer(nil)
66
+ class Body < Readable
67
+ def initialize(block, input = nil)
68
+ @block = block
69
+ @input = input
70
+ @output = nil
71
+ end
72
+
73
+ def stream?
74
+ true
75
+ end
76
+
77
+ # Invokes the block in a fiber which yields chunks when they are available.
78
+ def read
79
+ # We are reading chunk by chunk, allocate an output stream and execute the block to generate the chunks:
80
+ if @output.nil?
81
+ if @block.nil?
82
+ raise ConsumedError, "Streaming body has already been consumed!"
59
83
  end
84
+
85
+ @output = Output.schedule(@input, @block)
86
+ @block = nil
60
87
  end
88
+
89
+ @output.read
61
90
  end
62
91
 
63
- # Can be invoked by the block to write to the stream.
64
- def write(chunk)
65
- if from = @from
66
- @from = nil
67
- @from = from.transfer(chunk)
68
- else
69
- raise RuntimeError, "Stream is not being read!"
92
+ # Invoke the block with the given stream.
93
+ #
94
+ # The block can read and write to the stream, and must close the stream when finishing.
95
+ def call(stream)
96
+ if @block.nil?
97
+ raise ConsumedError, "Streaming block has already been consumed!"
70
98
  end
99
+
100
+ block = @block
101
+
102
+ @input = @output = @block = nil
103
+
104
+ # Ownership of the stream is passed into the block, in other words, the block is responsible for closing the stream.
105
+ block.call(stream)
106
+ rescue => error
107
+ # If, for some reason, the block raises an error, we assume it may not have closed the stream, so we close it here:
108
+ stream.close
109
+ raise
71
110
  end
72
111
 
73
- # Can be invoked by the block to close the stream.
112
+ # Closing a stream indicates we are no longer interested in reading from it.
74
113
  def close(error = nil)
75
- if from = @from
76
- @from = nil
77
- from.transfer(nil)
78
- elsif @fiber
79
- @fiber.raise(error || Closed)
114
+ if output = @output
115
+ @output = nil
116
+ # Closing the output here may take some time, as it may need to finish handling the stream:
117
+ output.close(error)
80
118
  end
81
- end
82
-
83
- def read
84
- raise RuntimeError, "Stream is already being read!" if @from
85
119
 
86
- @fiber&.transfer(Fiber.current)
120
+ if input = @input
121
+ @input = nil
122
+ input.close(error)
123
+ end
87
124
  end
88
125
  end
89
126
 
90
- # Invokes the block in a fiber which yields chunks when they are available.
91
- def read
92
- @output ||= Output.new(@input, @block)
127
+ # A deferred body has an extra `stream` method which can be used to stream data into the body, as the response body won't be available until the request has been sent.
128
+ class DeferredBody < Body
129
+ def initialize(block)
130
+ super(block, Writable.new)
131
+ end
93
132
 
94
- return @output.read
95
- end
96
-
97
- def stream?
98
- true
99
- end
100
-
101
- def call(stream)
102
- raise "Streaming body has already been read!" if @output
133
+ # Closing a stream indicates we are no longer interested in reading from it, but in this case that does not mean that the output block is finished generating data.
134
+ def close(error = nil)
135
+ if error
136
+ super
137
+ end
138
+ end
103
139
 
104
- @block.call(stream)
105
- rescue => error
106
- raise
107
- ensure
108
- self.close(error)
140
+ # Stream the response body into the block's input.
141
+ def stream(body)
142
+ body&.each do |chunk|
143
+ @input.write(chunk)
144
+ end
145
+ rescue => error
146
+ raise
147
+ ensure
148
+ @input.close_write(error)
149
+ end
109
150
  end
110
151
  end
111
152
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2018-2023, by Samuel Williams.
4
+ # Copyright, 2024, by Samuel Williams.
5
5
 
6
6
  require_relative 'readable'
7
7
 
@@ -16,15 +16,9 @@ module Protocol
16
16
  # @param [Integer] length The length of the response body if known.
17
17
  # @param [Async::Queue] queue Specify a different queue implementation, e.g. `Async::LimitedQueue.new(8)` to enable back-pressure streaming.
18
18
  def initialize(length = nil, queue: Thread::Queue.new)
19
- @queue = queue
20
-
21
19
  @length = length
22
-
20
+ @queue = queue
23
21
  @count = 0
24
-
25
- @finished = false
26
-
27
- @closed = false
28
22
  @error = nil
29
23
  end
30
24
 
@@ -34,18 +28,16 @@ module Protocol
34
28
 
35
29
  # Stop generating output; cause the next call to write to fail with the given error. Does not prevent existing chunks from being read. In other words, this indicates both that no more data will be or should be written to the body.
36
30
  def close(error = nil)
37
- unless @closed
38
- @queue.close
39
-
40
- @closed = true
41
- @error = error
42
- end
31
+ @error ||= error
32
+
33
+ @queue.clear
34
+ @queue.close
43
35
 
44
36
  super
45
37
  end
46
38
 
47
39
  def closed?
48
- @closed
40
+ @queue.closed?
49
41
  end
50
42
 
51
43
  def ready?
@@ -59,22 +51,71 @@ module Protocol
59
51
 
60
52
  # Read the next available chunk.
61
53
  def read
54
+ if @error
55
+ raise @error
56
+ end
57
+
62
58
  @queue.pop
63
59
  end
64
60
 
65
61
  # Write a single chunk to the body. Signal completion by calling `#finish`.
66
62
  def write(chunk)
67
- # If the reader breaks, the writer will break.
68
- # The inverse of this is less obvious (*)
69
- if @closed
63
+ if @queue.closed?
70
64
  raise(@error || Closed)
71
65
  end
72
66
 
73
- @count += 1
74
67
  @queue.push(chunk)
68
+ @count += 1
75
69
  end
76
70
 
77
- alias << write
71
+ def close_write(error = nil)
72
+ @error ||= error
73
+ @queue.close
74
+ end
75
+
76
+ class Output
77
+ def initialize(writable)
78
+ @writable = writable
79
+ @closed = false
80
+ end
81
+
82
+ def closed?
83
+ @closed || @writable.closed?
84
+ end
85
+
86
+ def write(chunk)
87
+ @writable.write(chunk)
88
+ end
89
+
90
+ alias << write
91
+
92
+ def close(error = nil)
93
+ @closed = true
94
+
95
+ if error
96
+ @writable.close(error)
97
+ else
98
+ @writable.close_write
99
+ end
100
+ end
101
+ end
102
+
103
+ # Create an output wrapper which can be used to write chunks to the body.
104
+ def output
105
+ output = Output.new(self)
106
+
107
+ unless block_given?
108
+ return output
109
+ end
110
+
111
+ begin
112
+ yield output
113
+ rescue => error
114
+ raise error
115
+ ensure
116
+ output.close(error)
117
+ end
118
+ end
78
119
 
79
120
  def inspect
80
121
  "\#<#{self.class} #{@count} chunks written, #{status}>"
@@ -5,6 +5,6 @@
5
5
 
6
6
  module Protocol
7
7
  module HTTP
8
- VERSION = "0.33.0"
8
+ VERSION = "0.34.0"
9
9
  end
10
10
  end
data/readme.md CHANGED
@@ -14,6 +14,8 @@ Provides abstractions for working with the HTTP protocol.
14
14
 
15
15
  Please see the [project documentation](https://socketry.github.io/protocol-http/) for more details.
16
16
 
17
+ - [Streaming](https://socketry.github.io/protocol-http/guides/streaming/index) - This guide gives an overview of how to implement streaming requests and responses.
18
+
17
19
  - [Getting Started](https://socketry.github.io/protocol-http/guides/getting-started/index) - This guide explains how to use `protocol-http` for building abstract HTTP interfaces.
18
20
 
19
21
  - [Design Overview](https://socketry.github.io/protocol-http/guides/design-overview/index) - This guide explains the high level design of `protocol-http` in the context of wider design patterns that can be used to implement HTTP clients and servers.
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: protocol-http
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.33.0
4
+ version: 0.34.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
@@ -47,7 +47,7 @@ cert_chain:
47
47
  Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8
48
48
  voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg=
49
49
  -----END CERTIFICATE-----
50
- date: 2024-09-05 00:00:00.000000000 Z
50
+ date: 2024-09-10 00:00:00.000000000 Z
51
51
  dependencies: []
52
52
  description:
53
53
  email:
metadata.gz.sig CHANGED
Binary file