connectrpc 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +501 -0
  4. data/docs/design.md +504 -0
  5. data/docs/development.md +566 -0
  6. data/docs/grpc-web.md +165 -0
  7. data/docs/grpc.md +75 -0
  8. data/docs/rbs.md +469 -0
  9. data/docs/streaming.md +251 -0
  10. data/exe/protoc-gen-connectrpc-ruby +23 -0
  11. data/lib/connectrpc/bindings/descriptors/status.bin +15 -0
  12. data/lib/connectrpc/bindings/google_protobuf.rb +92 -0
  13. data/lib/connectrpc/cancellation.rb +220 -0
  14. data/lib/connectrpc/client.rb +260 -0
  15. data/lib/connectrpc/compression.rb +120 -0
  16. data/lib/connectrpc/deadline.rb +81 -0
  17. data/lib/connectrpc/error.rb +72 -0
  18. data/lib/connectrpc/generator/names.rb +105 -0
  19. data/lib/connectrpc/generator/rbs_renderer.rb +93 -0
  20. data/lib/connectrpc/generator/renderer.rb +115 -0
  21. data/lib/connectrpc/generator/runtime_names.rb +84 -0
  22. data/lib/connectrpc/generator/schema.rb +218 -0
  23. data/lib/connectrpc/generator/streaming_methods.rb +29 -0
  24. data/lib/connectrpc/generator.rb +42 -0
  25. data/lib/connectrpc/get_query.rb +159 -0
  26. data/lib/connectrpc/grpc.rb +200 -0
  27. data/lib/connectrpc/grpc_client.rb +23 -0
  28. data/lib/connectrpc/grpc_server.rb +158 -0
  29. data/lib/connectrpc/grpc_web.rb +131 -0
  30. data/lib/connectrpc/grpc_web_cors.rb +135 -0
  31. data/lib/connectrpc/grpc_web_io.rb +156 -0
  32. data/lib/connectrpc/grpc_web_server.rb +207 -0
  33. data/lib/connectrpc/instrumentation.rb +216 -0
  34. data/lib/connectrpc/message_io.rb +47 -0
  35. data/lib/connectrpc/metadata.rb +105 -0
  36. data/lib/connectrpc/method.rb +68 -0
  37. data/lib/connectrpc/protocol.rb +189 -0
  38. data/lib/connectrpc/server.rb +398 -0
  39. data/lib/connectrpc/streaming/client.rb +104 -0
  40. data/lib/connectrpc/streaming/client_execution.rb +87 -0
  41. data/lib/connectrpc/streaming/client_scope.rb +105 -0
  42. data/lib/connectrpc/streaming/framing.rb +123 -0
  43. data/lib/connectrpc/streaming/framing_io.rb +61 -0
  44. data/lib/connectrpc/streaming/method.rb +33 -0
  45. data/lib/connectrpc/streaming/server.rb +122 -0
  46. data/lib/connectrpc/streaming/server_execution.rb +88 -0
  47. data/lib/connectrpc/streaming/server_terminal.rb +70 -0
  48. data/lib/connectrpc/streaming/session.rb +143 -0
  49. data/lib/connectrpc/streaming/session_completion.rb +134 -0
  50. data/lib/connectrpc/streaming/session_io.rb +107 -0
  51. data/lib/connectrpc/streaming/session_messages.rb +119 -0
  52. data/lib/connectrpc/streaming/strategy.rb +95 -0
  53. data/lib/connectrpc/streaming/streams.rb +116 -0
  54. data/lib/connectrpc/streaming/terminal.rb +52 -0
  55. data/lib/connectrpc/transports/net_http.rb +246 -0
  56. data/lib/connectrpc/version.rb +5 -0
  57. data/lib/connectrpc.rb +38 -0
  58. data/rbs/google_protobuf_4_36/runtime.rbs +41 -0
  59. data/sig/connectrpc.rbs +772 -0
  60. data/sig/manifest.yaml +2 -0
  61. data/third_party/README.md +16 -0
  62. data/third_party/google/LICENSE +201 -0
  63. data/third_party/google/rpc/status.proto +48 -0
  64. data/third_party/protobuf-LICENSE +32 -0
  65. metadata +106 -0
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ConnectRPC
4
+ module Streaming
5
+ class ClientScope
6
+ attr_accessor :completed, :abandoned, :independent_error, :failure, :result, :delivered_error
7
+
8
+ def initialize(lifecycle)
9
+ @lifecycle = lifecycle
10
+ @completed = false
11
+ @abandoned = false
12
+ end
13
+
14
+ def run(timeout, cancellation, &)
15
+ # @type var error: Exception?
16
+ error = nil
17
+ execute(timeout, cancellation, &)
18
+ rescue Exception => e # rubocop:disable Lint/RescueException -- Finalization needs the actual escape, including fatal exceptions.
19
+ error = e
20
+ raise
21
+ ensure
22
+ finish_preserving_user(error)
23
+ end
24
+
25
+ def execute(timeout, cancellation)
26
+ active, deadline = start(timeout, cancellation)
27
+ yield active, deadline
28
+ active.commit! if @completed
29
+ @result
30
+ rescue StandardError => e
31
+ @lifecycle&.record_failure(e)
32
+ @state&.commit! unless @independent_error || @abandoned
33
+ raise
34
+ end
35
+ private :execute
36
+
37
+ def cleanup(session)
38
+ return if @completed || @delivered_error
39
+
40
+ @abandoned = true if implicit_abandonment?
41
+ session.abandon unless session.abandoned?
42
+ rescue StandardError => e
43
+ @lifecycle&.record_failure(e)
44
+ end
45
+
46
+ def record_user_error(error)
47
+ @independent_error = error
48
+ @lifecycle&.record_failure(error)
49
+ end
50
+
51
+ private
52
+
53
+ def finish_preserving_user(error)
54
+ finish(@independent_error || error)
55
+ ensure
56
+ original = @independent_error
57
+ raise original, cause: original.cause if original
58
+ end
59
+
60
+ def finish(error)
61
+ @state&.close
62
+ lifecycle = @lifecycle
63
+ return unless lifecycle
64
+
65
+ lifecycle.record_failure(error) if error
66
+ report_processing(lifecycle, error)
67
+ lifecycle.finish(delivery: delivery_completed?(error) ? :completed : :aborted)
68
+ end
69
+
70
+ def start(timeout, cancellation)
71
+ deadline = Deadline.new(timeout) unless timeout.nil?
72
+ active = @state = CallCancellation.new(deadline: deadline, cancellation: cancellation)
73
+ active.check!
74
+ [active, deadline]
75
+ end
76
+
77
+ def implicit_abandonment?
78
+ !@independent_error && (@failure.nil? || @failure.is_a?(StreamNotConsumedError))
79
+ end
80
+
81
+ def success?(error) = @completed && error.nil?
82
+
83
+ def delivery_completed?(error)
84
+ return true if success?(error)
85
+
86
+ delivered = @delivered_error
87
+ return false unless delivered
88
+
89
+ delivered.equal?(error)
90
+ end
91
+
92
+ def report_processing(lifecycle, error)
93
+ normalized = normalized_error(error)
94
+ lifecycle.processing_finished(code: normalized&.code || ("OK" if success?(error)),
95
+ error: error, normalized_error: normalized)
96
+ end
97
+
98
+ def normalized_error(error)
99
+ return Error.new(:canceled, "Stream abandoned") if @abandoned
100
+
101
+ error if error.is_a?(Error)
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ConnectRPC
4
+ module Streaming
5
+ # @type var types: Hash[codec, String]
6
+ types = { proto: "application/connect+proto", json: "application/connect+json" }
7
+ CONTENT_TYPES = types.freeze
8
+ TERMINAL_LIMIT = 65_536
9
+ FRAGMENT_SIZE = 16_384
10
+ RECOVERY_LIMIT = 262_144
11
+ RECOVERY_SECONDS = 0.1
12
+ TAIL_SECONDS = 1.0
13
+
14
+ def self.codec(content_type)
15
+ return :proto if content_type&.match?(%r{\Aapplication/connect\+proto\z}i)
16
+ return :json if content_type&.match?(%r{\Aapplication/connect\+json\z}i)
17
+
18
+ nil
19
+ end
20
+
21
+ # Streaming content coding uses distinct protocol headers; reuse only the
22
+ # validated unary negotiation rules, never whole-stream compression.
23
+ def self.coding_headers(headers)
24
+ result = headers.except("content-encoding", "accept-encoding")
25
+ result["content-encoding"] = headers["connect-content-encoding"] if headers.key?("connect-content-encoding")
26
+ result["accept-encoding"] = headers["connect-accept-encoding"] if headers.key?("connect-accept-encoding")
27
+ result
28
+ end
29
+
30
+ class Framing
31
+ attr_reader :messages, :terminal
32
+ attr_writer :encoding
33
+
34
+ def initialize(input:, request:, wire_limit:, limit:, output: nil, encoding: :identity,
35
+ terminal_wire_limit: TERMINAL_LIMIT, terminal_limit: TERMINAL_LIMIT, state: nil, on_error: nil)
36
+ @input = input
37
+ @output = output
38
+ @request = request
39
+ @wire_limit = wire_limit
40
+ @limit = limit
41
+ @encoding = encoding
42
+ @terminal_wire_limit = terminal_wire_limit
43
+ @terminal_limit = terminal_limit
44
+ @on_error = on_error
45
+ initialize_state(state)
46
+ end
47
+
48
+ def read_frame(timeout: nil, skip_data: false)
49
+ prefix = exact(5, eof: true, timeout: timeout)
50
+ return if prefix.nil?
51
+
52
+ flags, length = prefix.unpack("CN") #: [Integer, Integer]
53
+ validate_envelope(flags, length)
54
+ bytes = payload(flags, length, timeout: timeout, skip_data: skip_data)
55
+ @terminal = true if flags.anybits?(2)
56
+ @messages += 1 unless @terminal
57
+ [flags, bytes]
58
+ end
59
+
60
+ def emit(bytes, terminal: false)
61
+ flags, bytes = prepare_payload(bytes, terminal)
62
+ @state&.check!
63
+ output = @output #: _StreamingWriter
64
+ output.write([flags, bytes.bytesize].pack("CN"))
65
+ emit_fragments(bytes, output)
66
+ nil
67
+ end
68
+
69
+ private
70
+
71
+ def initialize_state(state)
72
+ @state = state
73
+ @messages = 0
74
+ @terminal = false
75
+ end
76
+
77
+ def validate_envelope(flags, length)
78
+ validate_flags(flags)
79
+ bound = flags.anybits?(2) ? @terminal_wire_limit : @wire_limit
80
+ raise Error.new(:resource_exhausted, "Envelope exceeds wire byte limit"), cause: nil if length > bound
81
+ end
82
+
83
+ def validate_flags(flags)
84
+ protocol_error("Invalid envelope flags") unless [0, 1, 2, 3].include?(flags)
85
+ protocol_error("Request terminal is forbidden") if flags.anybits?(2) && @request
86
+ protocol_error("Envelope after terminal") if @terminal
87
+ raise Error.new(:internal, "Unnegotiated compression"), cause: nil if flags.anybits?(1) && @encoding != :gzip
88
+ end
89
+
90
+ def payload(flags, length, timeout:, skip_data:)
91
+ return skip_payload(length, timeout) if skip_data && !flags.anybits?(2)
92
+
93
+ bytes = exact(length, timeout: timeout) #: String
94
+ bound = flags.anybits?(2) ? @terminal_limit : @limit
95
+ bytes = inflate(bytes, bound) if flags.anybits?(1)
96
+ Protocol.check_size!(bytes, bound)
97
+ bytes
98
+ end
99
+
100
+ def inflate(bytes, bound)
101
+ protocol_error("Empty compressed envelope") if bytes.empty?
102
+ Compression.decode(bytes, limit: bound, deadline: @state, on_error: @on_error)
103
+ rescue Compression::Malformed, Zlib::Error => e
104
+ @on_error&.call(e)
105
+ protocol_error("Malformed compressed envelope")
106
+ end
107
+
108
+ def skip_payload(length, timeout)
109
+ remaining = length
110
+ while remaining.positive?
111
+ chunk = exact([remaining, FRAGMENT_SIZE].min, timeout: timeout) #: String
112
+ remaining -= chunk.bytesize
113
+ end
114
+ ""
115
+ end
116
+
117
+ def protocol_error(message)
118
+ raise Error.new(@request ? :invalid_argument : :internal, message), cause: nil
119
+ end
120
+ end
121
+ end
122
+ private_constant :Streaming
123
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ConnectRPC
4
+ module Streaming
5
+ class Framing
6
+ private
7
+
8
+ def prepare_payload(bytes, terminal)
9
+ bound = terminal ? @terminal_wire_limit : @wire_limit
10
+ Protocol.check_size!(bytes, terminal ? @terminal_limit : @limit)
11
+ flags = terminal ? 2 : 0
12
+ if !terminal && @encoding == :gzip
13
+ bytes = Compression.encode(bytes, limit: bound, deadline: @state, on_error: @on_error)
14
+ flags = 1
15
+ end
16
+ check_output_size(bytes, bound)
17
+ [flags, bytes]
18
+ end
19
+
20
+ def check_output_size(bytes, bound)
21
+ Protocol.check_size!(bytes, bound)
22
+ return if bytes.bytesize <= 0xffff_ffff
23
+
24
+ raise Error.new(:resource_exhausted, "Envelope exceeds uint32 length"), cause: nil
25
+ end
26
+
27
+ def emit_fragments(bytes, output)
28
+ offset = 0
29
+ while offset < bytes.bytesize
30
+ @state&.check!
31
+ fragment = bytes.byteslice(offset, FRAGMENT_SIZE) #: String
32
+ output.write(fragment)
33
+ offset += fragment.bytesize
34
+ end
35
+ end
36
+
37
+ def exact(length, eof: false, timeout: nil)
38
+ bytes = +"".b
39
+ while bytes.bytesize < length
40
+ chunk = read_chunk(length - bytes.bytesize, timeout)
41
+ if chunk.nil? || chunk.empty?
42
+ return if eof && bytes.empty?
43
+
44
+ protocol_error("Truncated envelope")
45
+ end
46
+ bytes << chunk
47
+ end
48
+ bytes
49
+ end
50
+
51
+ def read_chunk(length, timeout)
52
+ @state&.check!
53
+ input = @input #: _StreamingInput
54
+ chunk = input.read([length, FRAGMENT_SIZE].min, timeout: timeout)
55
+ @state&.check!
56
+ protocol_error("Input exceeded requested fragment") if chunk && chunk.bytesize > length
57
+ chunk
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ConnectRPC
4
+ # Streaming declarations share validation, but are never unary Methods.
5
+ class StreamDeclaration
6
+ attr_reader :service, :name, :request_type, :response_type, :path, :idempotency
7
+
8
+ def initialize(service:, name:, request_type:, response_type:, idempotency: :unknown)
9
+ declaration = Method.new(service: service, name: name, request_type: request_type,
10
+ response_type: response_type, idempotency: idempotency)
11
+ @service = declaration.service
12
+ @name = declaration.name
13
+ @request_type = declaration.request_type
14
+ @response_type = declaration.response_type
15
+ @path = declaration.path
16
+ @idempotency = declaration.idempotency
17
+ freeze
18
+ end
19
+ end
20
+ private_constant :StreamDeclaration
21
+
22
+ class ServerStreamMethod < StreamDeclaration
23
+ def cardinality = :server_stream
24
+ end
25
+
26
+ class ClientStreamMethod < StreamDeclaration
27
+ def cardinality = :client_stream
28
+ end
29
+
30
+ class BidiStreamMethod < StreamDeclaration
31
+ def cardinality = :bidi_stream
32
+ end
33
+ end
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ConnectRPC
4
+ class Server
5
+ # Explicit native-host SPI. Portable adapters retain the unary entry point.
6
+ def call_stream(path:, http_method:, headers:, input:, output:, publish:, lifecycle: nil, cancellation: nil, # rubocop:disable Metrics/MethodLength
7
+ http2: false, capabilities: nil)
8
+ capabilities = stream_capabilities(http2, capabilities)
9
+ unless capabilities
10
+ return call(path: path, http_method: http_method, headers: headers, input: input,
11
+ lifecycle: lifecycle, cancellation: cancellation)
12
+ end
13
+
14
+ if @methods[path].is_a?(BidiStreamMethod) && !capabilities.include?(:bidi_stream)
15
+ return bare(505, {}, cancellation: cancellation)
16
+ end
17
+
18
+ return bare(415, {}, cancellation: cancellation) unless @protocol.include?(:connect)
19
+
20
+ values = Protocol.headers(headers)
21
+ codec = Streaming.codec(Protocol.single(values, "content-type"))
22
+ rejection = stream_preflight(http_method, codec, cancellation)
23
+ return rejection if rejection || !codec
24
+
25
+ execute_native_stream(path, values, codec, http_method: http_method,
26
+ input: input, output: output, publish: publish,
27
+ lifecycle: lifecycle, cancellation: cancellation,
28
+ capabilities: capabilities)
29
+ end
30
+
31
+ private
32
+
33
+ def stream_capabilities(http2, capabilities)
34
+ # @type var shapes: Array[streaming_cardinality]
35
+ shapes = %i[server_stream client_stream bidi_stream]
36
+ return (http2 ? shapes : nil) if capabilities.nil?
37
+
38
+ raise ArgumentError, "Invalid streaming capabilities" if http2 || !stream_capability_list?(capabilities)
39
+
40
+ capabilities
41
+ end
42
+
43
+ def stream_capability_list?(capabilities)
44
+ # @type var shapes: Array[streaming_cardinality]
45
+ shapes = %i[server_stream client_stream bidi_stream]
46
+ capabilities.is_a?(Array) && !capabilities.empty? && capabilities.uniq == capabilities &&
47
+ (capabilities - shapes).empty?
48
+ end
49
+
50
+ def stream_preflight(http_method, codec, cancellation)
51
+ return bare(405, { "allow" => ["POST"] }, cancellation: cancellation) unless http_method == "POST"
52
+ return if codec
53
+
54
+ bare(415, { "accept-post" => [Streaming::CONTENT_TYPES.values.join(", ")] }, cancellation: cancellation)
55
+ end
56
+
57
+ def execute_native_stream(path, headers, codec, http_method:, input:, output:, publish:, lifecycle:,
58
+ cancellation:, capabilities:) # rubocop:disable Metrics/MethodLength
59
+ # @type var owned: bool?
60
+ owned = nil
61
+ # @type var error: Exception?
62
+ error = nil
63
+ lifecycle, owned = begin_native_processing(path, headers, lifecycle, http_method)
64
+ work = lambda {
65
+ process_stream(path, headers, codec, input: input, output: output, publish: publish,
66
+ lifecycle: lifecycle, cancellation: cancellation,
67
+ capabilities: capabilities)
68
+ }
69
+ lifecycle ? lifecycle.activate(&work) : work.call
70
+ nil
71
+ rescue Exception => e # rubocop:disable Lint/RescueException -- observe the actual escape and preserve it
72
+ error = e
73
+ raise
74
+ ensure
75
+ finish_native_processing(lifecycle, owned, error) unless owned.nil?
76
+ end
77
+
78
+ def begin_native_processing(path, headers, lifecycle, http_method)
79
+ owned = lifecycle.nil?
80
+ lifecycle ||= start_call(path: path, headers: headers, http_method: http_method)
81
+ lifecycle&.begin_processing(self, path)
82
+ [lifecycle, owned]
83
+ end
84
+
85
+ def finish_native_processing(lifecycle, owned, error)
86
+ return unless lifecycle
87
+
88
+ lifecycle.processing_finished(code: nil, error: error) unless lifecycle.outcome
89
+ lifecycle.finish(delivery: :unobserved) if owned
90
+ end
91
+
92
+ def process_stream(path, headers, codec, input:, output:, publish:, lifecycle:, cancellation:, capabilities:)
93
+ # @type var state: Streaming::ServerState?
94
+ active = state = Streaming::ServerState.new(headers: headers, codec: codec, publish: publish,
95
+ lifecycle: lifecycle, cancellation: cancellation)
96
+ capture_stream_error(active) do
97
+ require_stream_capability(path, capabilities)
98
+ execute_stream_handler(active, path, input, output)
99
+ end
100
+ render_stream_terminal(active, output)
101
+ ensure
102
+ state&.session&.close
103
+ state&.context&.close
104
+ end
105
+
106
+ def require_stream_capability(path, capabilities)
107
+ declaration = @methods[path]
108
+ return if !declaration || declaration.is_a?(Method) || capabilities.include?(declaration.cardinality)
109
+
110
+ raise Error.new(:unimplemented, "Unsupported streaming method"), cause: nil
111
+ end
112
+
113
+ def capture_stream_error(state)
114
+ yield
115
+ rescue StandardError => e
116
+ state.context&.record_failure(e)
117
+ state.lifecycle&.record_failure(e)
118
+ safe = e.is_a?(Error) ? e : Error.new(:internal, "Internal error")
119
+ state.error = terminal_error(safe, state.context, state.cancellation)
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ConnectRPC
4
+ module Streaming
5
+ # Mutable state is local to one original processing task, never the Server.
6
+ class ServerState
7
+ attr_reader :headers, :codec, :publish, :lifecycle, :cancellation
8
+ attr_accessor :context, :session, :encoding, :error
9
+
10
+ def initialize(headers:, codec:, publish:, lifecycle:, cancellation:)
11
+ @headers = headers
12
+ @codec = codec
13
+ @publish = publish
14
+ @lifecycle = lifecycle
15
+ @cancellation = cancellation
16
+ @encoding = :identity
17
+ end
18
+ end
19
+ end
20
+
21
+ class Server
22
+ private
23
+
24
+ def execute_stream_handler(state, path, input, output)
25
+ state.encoding = stream_encoding(state.headers)
26
+ context = state.context = streaming_context(state)
27
+ declaration = streaming_declaration(path)
28
+ session = state.session = new_stream_session(state, declaration, input, output)
29
+ invoke_stream_handler(@handlers.fetch(path), declaration, session, context)
30
+ context.check!
31
+ end
32
+
33
+ def stream_encoding(headers)
34
+ normalized = Streaming.coding_headers(headers)
35
+ validate_headers!(normalized)
36
+ request_encoding = Protocol.content_encoding(normalized) #: encoding
37
+ Protocol.response_encoding(normalized, request_encoding, @encoding)
38
+ end
39
+
40
+ def streaming_declaration(path)
41
+ declaration = @methods[path]
42
+ return declaration if declaration && !declaration.is_a?(Method) && @handlers[path]
43
+
44
+ raise Error.new(:unimplemented, "Unknown streaming method"), cause: nil
45
+ end
46
+
47
+ def new_stream_session(state, declaration, input, output)
48
+ context = state.context #: StreamingContext
49
+ coding = Protocol.content_encoding(Streaming.coding_headers(state.headers)) #: encoding
50
+ Streaming::Session.new(binding: @binding, method: declaration, codec: state.codec, state: context,
51
+ input: input, output: output, request_limit: @request_limit,
52
+ response_limit: @response_limit, request_wire_limit: @request_wire_limit,
53
+ response_wire_limit: @response_wire_limit,
54
+ request_encoding: coding,
55
+ response_encoding: state.encoding, terminal_wire_limit: @terminal_wire_limit,
56
+ terminal_limit: @terminal_limit, server: true, context: state.context,
57
+ lifecycle: state.lifecycle)
58
+ end
59
+
60
+ def invoke_stream_handler(handler, declaration, session, context) # rubocop:disable Metrics/MethodLength
61
+ context.check!
62
+ case declaration
63
+ when ServerStreamMethod
64
+ ss = handler #: _ServerStreamHandler[untyped, untyped]
65
+ request = single_stream_request(session)
66
+ context.check!
67
+ ss.call(request, ResponseWriter.new(session, context), context)
68
+ when ClientStreamMethod
69
+ cs = handler #: _ClientStreamHandler[untyped, untyped]
70
+ session.send_message(cs.call(RequestReader.new(session), context))
71
+ when BidiStreamMethod
72
+ invoke_bidi_stream(handler, session, context)
73
+ end
74
+ end
75
+
76
+ def invoke_bidi_stream(handler, session, context)
77
+ bidi = handler #: _BidiStreamHandler[untyped, untyped]
78
+ bidi.call(ServerDuplex.new(session, context), context)
79
+ end
80
+
81
+ def single_stream_request(session)
82
+ request = session.receive
83
+ return request if !request.nil? && session.receive.nil?
84
+
85
+ raise Error.new(:unimplemented, "Expected exactly one request"), cause: nil
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ConnectRPC
4
+ class Server
5
+ private
6
+
7
+ def render_stream_terminal(state, output)
8
+ context = state.context ||= streaming_context(state, headers: {})
9
+ merge_stream_error_metadata(state.error, context)
10
+ bytes, effective = stream_terminal_bytes(state, context)
11
+ handler_error = state.error
12
+ # A peer reset may reject terminal delivery. Retain the handler failure
13
+ # independently of that later delivery error.
14
+ observe_stream_result(state, effective) if handler_error
15
+ context.send(:publish_headers, check: false)
16
+ emit_stream_terminal(bytes, output)
17
+ context.commit! unless handler_error
18
+ observe_stream_result(state, effective) unless handler_error
19
+ end
20
+
21
+ def stream_terminal_bytes(state, context)
22
+ trailers = stream_trailers(state.error, context)
23
+ Streaming.terminal_bytes(state.error, trailers, wire_limit: @terminal_wire_limit, limit: @terminal_limit)
24
+ end
25
+
26
+ def merge_stream_error_metadata(error, context)
27
+ return unless error && !context.response_metadata.frozen?
28
+
29
+ error.metadata.each do |key, values|
30
+ context.response_metadata[key] = (context.response_metadata[key] || []) + values
31
+ end
32
+ end
33
+
34
+ def stream_trailers(error, context)
35
+ trailers = Metadata.new(context.response_trailers)
36
+ error&.trailers&.each { |key, values| trailers[key] = (trailers[key] || []) + values }
37
+ context.response_trailers.freeze
38
+ trailers
39
+ end
40
+
41
+ def emit_stream_terminal(bytes, output)
42
+ framing = Streaming::Framing.new(input: nil, output: output, request: false,
43
+ limit: @response_limit, wire_limit: @response_wire_limit,
44
+ terminal_wire_limit: @terminal_wire_limit, terminal_limit: @terminal_limit)
45
+ framing.emit(bytes, terminal: true)
46
+ end
47
+
48
+ def observe_stream_result(state, effective)
49
+ context = state.context #: StreamingContext
50
+ state.lifecycle&.processing_finished(code: effective ? effective.code : "OK", error: state.error,
51
+ normalized_error: effective, http_status: 200,
52
+ response_metadata: Instrumentation.carrier(context.response_metadata))
53
+ end
54
+
55
+ def streaming_context(state, headers: state.headers)
56
+ metadata = Protocol.metadata(headers)
57
+ state.lifecycle.request_metadata = Instrumentation.carrier(metadata) if state.lifecycle
58
+ leading = ->(snapshot) { state.publish.call(stream_head(snapshot, state.codec, state.encoding)) }
59
+ StreamingContext.new(publish: leading, request_metadata: metadata, deadline: request_deadline(headers),
60
+ cancellation: state.cancellation, lifecycle: state.lifecycle)
61
+ end
62
+
63
+ def stream_head(metadata, codec, encoding)
64
+ values = metadata.to_h.merge("content-type" => [Streaming::CONTENT_TYPES.fetch(codec)])
65
+ values["connect-content-encoding"] = ["gzip"] if encoding == :gzip
66
+ values["connect-accept-encoding"] = ["gzip, identity"] if @encoding == :gzip
67
+ StreamHead.new(status: 200, headers: values)
68
+ end
69
+ end
70
+ end