Skip to content

Commit fe38f9f

Browse files
committed
100% documentation coverage.
1 parent 07b102b commit fe38f9f

20 files changed

+471
-2
lines changed

lib/protocol/http2/client.rb

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,45 @@
77

88
module Protocol
99
module HTTP2
10+
# Represents an HTTP/2 client connection.
11+
# Manages client-side protocol semantics including stream ID allocation,
12+
# connection preface handling, and push promise processing.
1013
class Client < Connection
14+
# Initialize a new HTTP/2 client connection.
15+
# @parameter framer [Framer] The frame handler for reading/writing HTTP/2 frames.
1116
def initialize(framer)
1217
super(framer, 1)
1318
end
1419

20+
# Check if the given stream ID represents a locally-initiated stream.
21+
# Client streams have odd numbered IDs.
22+
# @parameter id [Integer] The stream ID to check.
23+
# @returns [bool] True if the stream ID is locally-initiated.
1524
def local_stream_id?(id)
1625
id.odd?
1726
end
1827

28+
# Check if the given stream ID represents a remotely-initiated stream.
29+
# Server streams have even numbered IDs.
30+
# @parameter id [Integer] The stream ID to check.
31+
# @returns [bool] True if the stream ID is remotely-initiated.
1932
def remote_stream_id?(id)
2033
id.even?
2134
end
2235

36+
# Check if the given stream ID is valid for remote initiation.
37+
# Server-initiated streams must have even numbered IDs.
38+
# @parameter stream_id [Integer] The stream ID to validate.
39+
# @returns [bool] True if the stream ID is valid for remote initiation.
2340
def valid_remote_stream_id?(stream_id)
2441
stream_id.even?
2542
end
2643

44+
# Send the HTTP/2 connection preface and initial settings.
45+
# This must be called once when the connection is first established.
46+
# @parameter settings [Array] Optional settings to send with the connection preface.
47+
# @raises [ProtocolError] If called when not in the new state.
48+
# @yields Allows custom processing during preface exchange.
2749
def send_connection_preface(settings = [])
2850
if @state == :new
2951
@framer.write_connection_preface
@@ -42,10 +64,15 @@ def send_connection_preface(settings = [])
4264
end
4365
end
4466

67+
# Clients cannot create push promise streams.
68+
# @raises [ProtocolError] Always, as clients cannot initiate push promises.
4569
def create_push_promise_stream
4670
raise ProtocolError, "Cannot create push promises from client!"
4771
end
4872

73+
# Process a push promise frame received from the server.
74+
# @parameter frame [PushPromiseFrame] The push promise frame to process.
75+
# @returns [Array(Stream, Hash) | Nil] The promised stream and request headers, or nil if no associated stream.
4976
def receive_push_promise(frame)
5077
if frame.stream_id == 0
5178
raise ProtocolError, "Cannot receive headers for stream 0!"

lib/protocol/http2/connection.rb

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,14 @@
1212

1313
module Protocol
1414
module HTTP2
15+
# This is the core connection class that handles HTTP/2 protocol semantics including
16+
# stream management, settings negotiation, and frame processing.
1517
class Connection
1618
include FlowControlled
1719

20+
# Initialize a new HTTP/2 connection.
21+
# @parameter framer [Framer] The frame handler for reading/writing HTTP/2 frames.
22+
# @parameter local_stream_id [Integer] The starting stream ID for locally-initiated streams.
1823
def initialize(framer, local_stream_id)
1924
super()
2025

@@ -41,10 +46,15 @@ def initialize(framer, local_stream_id)
4146
@remote_window = Window.new
4247
end
4348

49+
# The connection stream ID (always 0 for connection-level operations).
50+
# @returns [Integer] Always returns 0 for the connection itself.
4451
def id
4552
0
4653
end
4754

55+
# Access streams by ID, with 0 returning the connection itself.
56+
# @parameter id [Integer] The stream ID to look up.
57+
# @returns [Connection | Stream | Nil] The connection (if id=0), stream, or nil.
4858
def [] id
4959
if id.zero?
5060
self
@@ -89,6 +99,9 @@ def closed?
8999
@state == :closed || @framer.nil?
90100
end
91101

102+
# Remove a stream from the active streams collection.
103+
# @parameter id [Integer] The stream ID to remove.
104+
# @returns [Stream | Nil] The removed stream, or nil if not found.
92105
def delete(id)
93106
@streams.delete(id)
94107
end
@@ -106,10 +119,17 @@ def close(error = nil)
106119
end
107120
end
108121

122+
# Encode headers using HPACK compression.
123+
# @parameter headers [Array] The headers to encode.
124+
# @parameter buffer [String] Optional buffer for encoding output.
125+
# @returns [String] The encoded header block.
109126
def encode_headers(headers, buffer = String.new.b)
110127
HPACK::Compressor.new(buffer, @encoder, table_size_limit: @remote_settings.header_table_size).encode(headers)
111128
end
112129

130+
# Decode headers using HPACK decompression.
131+
# @parameter data [String] The encoded header block data.
132+
# @returns [Array] The decoded headers.
113133
def decode_headers(data)
114134
HPACK::Decompressor.new(data, @decoder, table_size_limit: @local_settings.header_table_size).decode
115135
end
@@ -141,6 +161,9 @@ def ignore_frame?(frame)
141161
end
142162
end
143163

164+
# Execute a block within a synchronized context.
165+
# This method provides a synchronization primitive for thread safety.
166+
# @yields The block to execute within the synchronized context.
144167
def synchronize
145168
yield
146169
end
@@ -171,6 +194,8 @@ def read_frame
171194
raise
172195
end
173196

197+
# Send updated settings to the remote peer.
198+
# @parameter changes [Hash] The settings changes to send.
174199
def send_settings(changes)
175200
@local_settings.append(changes)
176201

@@ -197,6 +222,9 @@ def send_goaway(error_code = 0, message = "")
197222
self.close!
198223
end
199224

225+
# Process a GOAWAY frame from the remote peer.
226+
# @parameter frame [GoawayFrame] The GOAWAY frame to process.
227+
# @raises [GoawayError] If the frame indicates a connection error.
200228
def receive_goaway(frame)
201229
# We capture the last stream that was processed.
202230
@remote_stream_id, error_code, message = frame.unpack
@@ -209,6 +237,8 @@ def receive_goaway(frame)
209237
end
210238
end
211239

240+
# Write a single frame to the connection.
241+
# @parameter frame [Frame] The frame to write.
212242
def write_frame(frame)
213243
synchronize do
214244
@framer.write_frame(frame)
@@ -217,6 +247,10 @@ def write_frame(frame)
217247
@framer.flush
218248
end
219249

250+
# Write multiple frames within a synchronized block.
251+
# @yields {|framer| ...} The framer for writing multiple frames.
252+
# @parameter framer [Framer] The framer instance.
253+
# @raises [EOFError] If the connection is closed.
220254
def write_frames
221255
if @framer
222256
synchronize do
@@ -229,6 +263,8 @@ def write_frames
229263
end
230264
end
231265

266+
# Update local settings and adjust stream window capacities.
267+
# @parameter changes [Hash] The settings changes to apply locally.
232268
def update_local_settings(changes)
233269
capacity = @local_settings.initial_window_size
234270

@@ -239,6 +275,8 @@ def update_local_settings(changes)
239275
@local_window.desired = capacity
240276
end
241277

278+
# Update remote settings and adjust stream window capacities.
279+
# @parameter changes [Hash] The settings changes to apply to remote peer.
242280
def update_remote_settings(changes)
243281
capacity = @remote_settings.initial_window_size
244282

@@ -273,12 +311,17 @@ def process_settings(frame)
273311
end
274312
end
275313

314+
# Transition the connection to the open state.
315+
# @returns [Connection] Self for method chaining.
276316
def open!
277317
@state = :open
278318

279319
return self
280320
end
281321

322+
# Receive and process a SETTINGS frame from the remote peer.
323+
# @parameter frame [SettingsFrame] The settings frame to process.
324+
# @raises [ProtocolError] If the connection is in an invalid state.
282325
def receive_settings(frame)
283326
if @state == :new
284327
# We transition to :open when we receive acknowledgement of first settings frame:
@@ -290,6 +333,8 @@ def receive_settings(frame)
290333
end
291334
end
292335

336+
# Send a PING frame to the remote peer.
337+
# @parameter data [String] The 8-byte ping payload data.
293338
def send_ping(data)
294339
if @state != :closed
295340
frame = PingFrame.new
@@ -301,6 +346,9 @@ def send_ping(data)
301346
end
302347
end
303348

349+
# Process a PING frame from the remote peer.
350+
# @parameter frame [PingFrame] The ping frame to process.
351+
# @raises [ProtocolError] If ping is received in invalid state.
304352
def receive_ping(frame)
305353
if @state != :closed
306354
# This is handled in `read_payload`:
@@ -318,6 +366,9 @@ def receive_ping(frame)
318366
end
319367
end
320368

369+
# Process a DATA frame from the remote peer.
370+
# @parameter frame [DataFrame] The data frame to process.
371+
# @raises [ProtocolError] If data is received for invalid stream.
321372
def receive_data(frame)
322373
update_local_window(frame)
323374

@@ -330,6 +381,10 @@ def receive_data(frame)
330381
end
331382
end
332383

384+
# Check if the given stream ID is valid for remote initiation.
385+
# This method should be overridden by client/server implementations.
386+
# @parameter stream_id [Integer] The stream ID to validate.
387+
# @returns [Boolean] True if the stream ID is valid for remote initiation.
333388
def valid_remote_stream_id?(stream_id)
334389
false
335390
end
@@ -366,6 +421,10 @@ def create_stream(id = next_stream_id, &block)
366421
end
367422
end
368423

424+
# Create a push promise stream.
425+
# This method should be overridden by client/server implementations.
426+
# @yields {|stream| ...} Optional block to configure the created stream.
427+
# @returns [Stream] The created push promise stream.
369428
def create_push_promise_stream(&block)
370429
create_stream(&block)
371430
end
@@ -397,10 +456,16 @@ def receive_headers(frame)
397456
end
398457
end
399458

459+
# Receive and process a PUSH_PROMISE frame.
460+
# @parameter frame [PushPromiseFrame] The push promise frame.
461+
# @raises [ProtocolError] Always raises as push promises are not supported.
400462
def receive_push_promise(frame)
401463
raise ProtocolError, "Unable to receive push promise!"
402464
end
403465

466+
# Receive and process a PRIORITY_UPDATE frame.
467+
# @parameter frame [PriorityUpdateFrame] The priority update frame.
468+
# @raises [ProtocolError] If the stream ID is invalid.
404469
def receive_priority_update(frame)
405470
if frame.stream_id != 0
406471
raise ProtocolError, "Invalid stream id: #{frame.stream_id}"
@@ -414,14 +479,25 @@ def receive_priority_update(frame)
414479
end
415480
end
416481

482+
# Check if the given stream ID represents a client-initiated stream.
483+
# Client streams always have odd numbered IDs.
484+
# @parameter id [Integer] The stream ID to check.
485+
# @returns [Boolean] True if the stream ID is client-initiated.
417486
def client_stream_id?(id)
418487
id.odd?
419488
end
420489

490+
# Check if the given stream ID represents a server-initiated stream.
491+
# Server streams always have even numbered IDs.
492+
# @parameter id [Integer] The stream ID to check.
493+
# @returns [Boolean] True if the stream ID is server-initiated.
421494
def server_stream_id?(id)
422495
id.even?
423496
end
424497

498+
# Check if the given stream ID represents an idle stream.
499+
# @parameter id [Integer] The stream ID to check.
500+
# @returns [Boolean] True if the stream ID is idle (not yet used).
425501
def idle_stream_id?(id)
426502
if id.even?
427503
# Server-initiated streams are even.
@@ -450,6 +526,9 @@ def closed_stream_id?(id)
450526
end
451527
end
452528

529+
# Receive and process a RST_STREAM frame.
530+
# @parameter frame [ResetStreamFrame] The reset stream frame.
531+
# @raises [ProtocolError] If the frame is invalid for connection context.
453532
def receive_reset_stream(frame)
454533
if frame.connection?
455534
raise ProtocolError, "Cannot reset connection!"
@@ -475,6 +554,8 @@ def consume_window(size = self.available_size)
475554
end
476555
end
477556

557+
# Receive and process a WINDOW_UPDATE frame.
558+
# @parameter frame [WindowUpdateFrame] The window update frame.
478559
def receive_window_update(frame)
479560
if frame.connection?
480561
super
@@ -494,10 +575,15 @@ def receive_window_update(frame)
494575
end
495576
end
496577

578+
# Receive and process a CONTINUATION frame.
579+
# @parameter frame [ContinuationFrame] The continuation frame.
580+
# @raises [ProtocolError] Always raises as unexpected continuation frames are not supported.
497581
def receive_continuation(frame)
498582
raise ProtocolError, "Received unexpected continuation: #{frame.class}"
499583
end
500584

585+
# Receive and process a generic frame (default handler).
586+
# @parameter frame [Frame] The frame to receive.
501587
def receive_frame(frame)
502588
# ignore.
503589
end

0 commit comments

Comments
 (0)