quicsilver 0.2.0 → 0.4.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 (81) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +4 -5
  3. data/.github/workflows/cibuildgem.yaml +93 -0
  4. data/.gitignore +3 -1
  5. data/CHANGELOG.md +81 -0
  6. data/Gemfile.lock +26 -4
  7. data/README.md +95 -31
  8. data/Rakefile +95 -3
  9. data/benchmarks/components.rb +191 -0
  10. data/benchmarks/concurrent.rb +110 -0
  11. data/benchmarks/helpers.rb +88 -0
  12. data/benchmarks/quicsilver_server.rb +1 -1
  13. data/benchmarks/rails.rb +170 -0
  14. data/benchmarks/throughput.rb +113 -0
  15. data/examples/README.md +44 -91
  16. data/examples/benchmark.rb +111 -0
  17. data/examples/connection_pool_demo.rb +47 -0
  18. data/examples/example_helper.rb +18 -0
  19. data/examples/falcon_middleware.rb +44 -0
  20. data/examples/feature_demo.rb +125 -0
  21. data/examples/grpc_style.rb +97 -0
  22. data/examples/minimal_http3_server.rb +6 -18
  23. data/examples/priorities.rb +60 -0
  24. data/examples/protocol_http_server.rb +31 -0
  25. data/examples/rack_http3_server.rb +8 -20
  26. data/examples/rails_feature_test.rb +260 -0
  27. data/examples/simple_client_test.rb +2 -2
  28. data/examples/streaming_sse.rb +33 -0
  29. data/examples/trailers.rb +69 -0
  30. data/ext/quicsilver/extconf.rb +14 -0
  31. data/ext/quicsilver/quicsilver.c +568 -181
  32. data/lib/quicsilver/client/client.rb +349 -0
  33. data/lib/quicsilver/client/connection_pool.rb +106 -0
  34. data/lib/quicsilver/client/request.rb +98 -0
  35. data/lib/quicsilver/libmsquic.2.dylib +0 -0
  36. data/lib/quicsilver/protocol/adapter.rb +176 -0
  37. data/lib/quicsilver/protocol/control_stream_parser.rb +106 -0
  38. data/lib/quicsilver/protocol/frame_parser.rb +142 -0
  39. data/lib/quicsilver/protocol/frame_reader.rb +55 -0
  40. data/lib/quicsilver/{http3.rb → protocol/frames.rb} +146 -30
  41. data/lib/quicsilver/protocol/priority.rb +56 -0
  42. data/lib/quicsilver/protocol/qpack/decoder.rb +165 -0
  43. data/lib/quicsilver/protocol/qpack/encoder.rb +227 -0
  44. data/lib/quicsilver/protocol/qpack/header_block_decoder.rb +140 -0
  45. data/lib/quicsilver/protocol/qpack/huffman.rb +459 -0
  46. data/lib/quicsilver/protocol/request_encoder.rb +47 -0
  47. data/lib/quicsilver/protocol/request_parser.rb +275 -0
  48. data/lib/quicsilver/protocol/response_encoder.rb +97 -0
  49. data/lib/quicsilver/protocol/response_parser.rb +141 -0
  50. data/lib/quicsilver/protocol/stream_input.rb +98 -0
  51. data/lib/quicsilver/protocol/stream_output.rb +59 -0
  52. data/lib/quicsilver/quicsilver.bundle +0 -0
  53. data/lib/quicsilver/server/listener_data.rb +14 -0
  54. data/lib/quicsilver/server/request_handler.rb +138 -0
  55. data/lib/quicsilver/server/request_registry.rb +50 -0
  56. data/lib/quicsilver/server/server.rb +610 -0
  57. data/lib/quicsilver/transport/configuration.rb +141 -0
  58. data/lib/quicsilver/transport/connection.rb +379 -0
  59. data/lib/quicsilver/transport/event_loop.rb +38 -0
  60. data/lib/quicsilver/transport/inbound_stream.rb +33 -0
  61. data/lib/quicsilver/transport/stream.rb +28 -0
  62. data/lib/quicsilver/transport/stream_event.rb +26 -0
  63. data/lib/quicsilver/version.rb +1 -1
  64. data/lib/quicsilver.rb +55 -14
  65. data/lib/rackup/handler/quicsilver.rb +1 -2
  66. data/quicsilver.gemspec +13 -3
  67. metadata +125 -21
  68. data/benchmarks/benchmark.rb +0 -68
  69. data/examples/setup_certs.sh +0 -57
  70. data/lib/quicsilver/client.rb +0 -261
  71. data/lib/quicsilver/connection.rb +0 -42
  72. data/lib/quicsilver/event_loop.rb +0 -38
  73. data/lib/quicsilver/http3/request_encoder.rb +0 -133
  74. data/lib/quicsilver/http3/request_parser.rb +0 -176
  75. data/lib/quicsilver/http3/response_encoder.rb +0 -186
  76. data/lib/quicsilver/http3/response_parser.rb +0 -160
  77. data/lib/quicsilver/listener_data.rb +0 -29
  78. data/lib/quicsilver/quic_stream.rb +0 -36
  79. data/lib/quicsilver/request_registry.rb +0 -48
  80. data/lib/quicsilver/server.rb +0 -355
  81. data/lib/quicsilver/server_configuration.rb +0 -78
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Example: Running a Rack app with protocol-http as the internal layer
5
+ #
6
+ # quicsilver uses protocol-http Request/Response internally.
7
+ # Rack apps are automatically wrapped — no code changes needed.
8
+ #
9
+ # Modes:
10
+ # :rack (default) — Rack app, wrapped with Protocol::Rack::Adapter
11
+ # :falcon — native protocol-http app, no wrapping
12
+
13
+ require_relative "example_helper"
14
+
15
+ # Any standard Rack app works
16
+ app = ->(env) {
17
+ puts "#{env['REQUEST_METHOD']} #{env['PATH_INFO']} #{env['SERVER_PROTOCOL']}"
18
+
19
+ body = "Hello from Quicsilver!\n" \
20
+ "Method: #{env['REQUEST_METHOD']}\n" \
21
+ "Path: #{env['PATH_INFO']}\n" \
22
+ "Protocol: #{env['SERVER_PROTOCOL']}\n"
23
+
24
+ [200, { "content-type" => "text/plain" }, [body]]
25
+ }
26
+
27
+ server = Quicsilver::Server.new(4433, app: app, server_configuration: EXAMPLE_TLS_CONFIG)
28
+
29
+ puts "Starting Quicsilver on https://localhost:4433"
30
+ puts "Test with: curl --http3 -k https://localhost:4433/"
31
+ server.start
@@ -1,7 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require "bundler/setup"
4
- require "quicsilver"
3
+ require_relative "example_helper"
5
4
 
6
5
  puts "🚀 Rack HTTP/3 Server Example"
7
6
  puts "=" * 40
@@ -31,24 +30,13 @@ app = ->(env) {
31
30
  end
32
31
  }
33
32
 
34
- # Create and start the server with the Rack app
35
- server = Quicsilver::Server.new(4433, app: app)
33
+ server = Quicsilver::Server.new(4433, app: app, server_configuration: EXAMPLE_TLS_CONFIG)
36
34
 
37
- puts "🔧 Starting server..."
38
- server.start
39
-
40
- puts "✅ Server is running on port 4433"
41
35
  puts "📋 Try these requests:"
42
- puts " curl --http3 -k https://127.0.0.1:4433/"
43
- puts " curl --http3 -k https://127.0.0.1:4433/api/users"
44
- puts " curl --http3 -k https://127.0.0.1:4433/api/status"
36
+ puts " curl --http3 -k https://localhost:4433/"
37
+ puts " curl --http3 -k https://localhost:4433/api/users"
38
+ puts " curl --http3 -k https://localhost:4433/api/status"
39
+ puts "⏳ Press Ctrl+C to stop."
40
+ puts
45
41
 
46
- # Keep the server running
47
- puts "⏳ Server is running. Press Ctrl+C to stop..."
48
- begin
49
- server.wait_for_connections
50
- rescue Interrupt
51
- puts "\n🛑 Stopping server..."
52
- server.stop
53
- puts "👋 Server stopped"
54
- end
42
+ server.start
@@ -0,0 +1,260 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Tests all major quicsilver features against a running Rails app.
4
+ #
5
+ # Prerequisites:
6
+ # 1. Start the Rails app: cd blogz && bundle exec rackup -s quicsilver -p 4433
7
+ # 2. Run this script: cd quicsilver && bundle exec ruby examples/rails_feature_test.rb
8
+ #
9
+ # Tests: ping, streaming, priorities, echo, HEAD, multiplexing, connection pooling
10
+
11
+ require "bundler/setup"
12
+ require "quicsilver"
13
+
14
+ HOST = "localhost"
15
+ PORT = 4433
16
+ PASS = "✅"
17
+ FAIL = "❌"
18
+
19
+ def test(name)
20
+ result = yield
21
+ puts " #{PASS} #{name}"
22
+ result
23
+ rescue => e
24
+ puts " #{FAIL} #{name}: #{e.message}"
25
+ nil
26
+ end
27
+
28
+ puts "🚀 Quicsilver Rails Feature Test"
29
+ puts " Target: https://#{HOST}:#{PORT}"
30
+ puts "=" * 60
31
+
32
+ # === 1. Basic connectivity ===
33
+ puts "\n1️⃣ Basic Connectivity"
34
+ puts "-" * 60
35
+
36
+ test("GET /h3/ping returns JSON over HTTP/3") do
37
+ response = Quicsilver::Client.get(HOST, PORT, "/h3/ping", unsecure: true)
38
+ raise "Expected 200, got #{response[:status]}" unless response[:status] == 200
39
+ raise "Expected JSON" unless response[:body].include?('"status":"ok"')
40
+ raise "Expected HTTP/3" unless response[:body].include?("HTTP/3")
41
+ puts " Response: #{response[:body]}"
42
+ end
43
+
44
+ # === 2. Connection Pooling ===
45
+ puts "\n2️⃣ Connection Pooling"
46
+ puts "-" * 60
47
+
48
+ test("Multiple requests reuse the same connection") do
49
+ 5.times do |i|
50
+ t = Process.clock_gettime(Process::CLOCK_MONOTONIC)
51
+ Quicsilver::Client.get(HOST, PORT, "/h3/ping", unsecure: true)
52
+ elapsed = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - t) * 1000).round(1)
53
+ puts " Request #{i}: #{elapsed}ms (#{i == 0 ? 'new connection' : 'same connection'})"
54
+ end
55
+ puts " Pool: #{Quicsilver::Client.pool.size} connection(s)"
56
+ raise "Pool should have 1 connection" unless Quicsilver::Client.pool.size == 1
57
+ end
58
+
59
+ # === 3. Streaming ===
60
+ puts "\n3️⃣ Streaming Response"
61
+ puts "-" * 60
62
+
63
+ test("GET /h3/stream returns chunked SSE data") do
64
+ response = Quicsilver::Client.get(HOST, PORT, "/h3/stream", unsecure: true)
65
+ raise "Expected 200" unless response[:status] == 200
66
+ chunks = response[:body].scan(/data: chunk \d+/)
67
+ raise "Expected 5 chunks, got #{chunks.size}" unless chunks.size == 5
68
+ puts " Received #{chunks.size} streamed chunks"
69
+ end
70
+
71
+ # === 4. POST with Body (Echo) ===
72
+ puts "\n4️⃣ POST with Body"
73
+ puts "-" * 60
74
+
75
+ test("POST /h3/echo returns the request body") do
76
+ body = '{"message": "Hello HTTP/3!"}'
77
+ response = Quicsilver::Client.post(HOST, PORT, "/h3/echo",
78
+ body: body,
79
+ headers: { "content-type" => "application/json" },
80
+ unsecure: true)
81
+ raise "Expected 200" unless response[:status] == 200
82
+ raise "Body not echoed" unless response[:body].include?("Hello HTTP/3!")
83
+ puts " Echoed: #{response[:body][0..80]}"
84
+ end
85
+
86
+ # === 5. HEAD Request ===
87
+ puts "\n5️⃣ HEAD Request"
88
+ puts "-" * 60
89
+
90
+ test("HEAD /h3/head returns headers but no body") do
91
+ client = Quicsilver::Client.new(HOST, PORT, unsecure: true)
92
+ response = client.head("/h3/head")
93
+ raise "Expected 200" unless response[:status] == 200
94
+ raise "HEAD should have empty body" unless response[:body].nil? || response[:body].empty?
95
+ puts " Status: #{response[:status]}, body size: #{response[:body]&.bytesize || 0}"
96
+ client.disconnect
97
+ end
98
+
99
+ # === 6. Multiplexing (Sequential on single connection) ===
100
+ puts "\n6️⃣ Multiplexing (10 requests, single connection)"
101
+ puts "-" * 60
102
+
103
+ test("10 sequential requests on one connection") do
104
+ client = Quicsilver::Client.new(HOST, PORT, unsecure: true)
105
+ t_start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
106
+ 10.times { client.get("/h3/ping") }
107
+ total = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - t_start) * 1000).round(1)
108
+ puts " 10 requests in #{total}ms (single connection)"
109
+ client.disconnect
110
+ end
111
+
112
+ # === 7. Priority Endpoints (CSS vs Image) ===
113
+ puts "\n7️⃣ Priority Endpoints"
114
+ puts "-" * 60
115
+
116
+ test("GET /h3/css returns CSS content") do
117
+ response = Quicsilver::Client.get(HOST, PORT, "/h3/css", unsecure: true)
118
+ raise "Expected 200" unless response[:status] == 200
119
+ puts " CSS: #{response[:body].bytesize} bytes"
120
+ end
121
+
122
+ test("GET /h3/image returns large binary payload") do
123
+ response = Quicsilver::Client.get(HOST, PORT, "/h3/image", unsecure: true)
124
+ raise "Expected 200" unless response[:status] == 200
125
+ raise "Expected 50KB" unless response[:body].bytesize == 50_000
126
+ puts " Image: #{response[:body].bytesize} bytes"
127
+ end
128
+
129
+ # === 8. Slow Response (Delay) ===
130
+ puts "\n8️⃣ Slow Response"
131
+ puts "-" * 60
132
+
133
+ test("GET /h3/slow?delay=0.2 respects delay") do
134
+ t = Process.clock_gettime(Process::CLOCK_MONOTONIC)
135
+ response = Quicsilver::Client.get(HOST, PORT, "/h3/slow?delay=0.2", unsecure: true)
136
+ elapsed = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - t) * 1000).round(1)
137
+ raise "Expected 200" unless response[:status] == 200
138
+ raise "Should take ~200ms, took #{elapsed}ms" unless elapsed > 150
139
+ puts " Delayed response in #{elapsed}ms"
140
+ end
141
+
142
+ # === 9. Concurrent Multiplexing ===
143
+ puts "\n9️⃣ Concurrent Multiplexing"
144
+ puts "-" * 60
145
+
146
+ test("4 concurrent requests on separate connections") do
147
+ threads = 4.times.map do |i|
148
+ Thread.new do
149
+ t = Process.clock_gettime(Process::CLOCK_MONOTONIC)
150
+ response = Quicsilver::Client.get(HOST, PORT, "/h3/slow?delay=0.2", unsecure: true)
151
+ elapsed = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - t) * 1000).round(1)
152
+ [i, response[:status], elapsed]
153
+ end
154
+ end
155
+ results = threads.map(&:value).sort_by(&:first)
156
+ results.each { |i, status, elapsed| puts " Stream #{i}: #{status} — #{elapsed}ms" }
157
+ total = results.map { |_, _, e| e }.max
158
+ # All 4 should complete in roughly the same time (~200ms) not 4x (800ms)
159
+ raise "Expected concurrent, took #{total}ms" if total > 600
160
+ puts " All 4 concurrent in #{total}ms (not #{200 * 4}ms sequential)"
161
+ end
162
+
163
+ # === 10. Large Upload ===
164
+ puts "\n🔟 Large Upload"
165
+ puts "-" * 60
166
+
167
+ test("POST 100KB body") do
168
+ big_body = "x" * 100_000
169
+ response = Quicsilver::Client.post(HOST, PORT, "/h3/upload",
170
+ body: big_body,
171
+ headers: { "content-type" => "application/octet-stream" },
172
+ unsecure: true)
173
+ raise "Expected 200" unless response[:status] == 200
174
+ raise "Size mismatch" unless response[:body].include?('"received_bytes":100000')
175
+ puts " Uploaded 100KB, server received 100,000 bytes"
176
+ end
177
+
178
+ # === 11. Content-Length Validation ===
179
+ puts "\n1️⃣1️⃣ Content-Length Validation"
180
+ puts "-" * 60
181
+
182
+ test("POST with content-length header") do
183
+ body = "hello"
184
+ response = Quicsilver::Client.post(HOST, PORT, "/h3/upload",
185
+ body: body,
186
+ headers: { "content-type" => "text/plain" },
187
+ unsecure: true)
188
+ raise "Expected 200, got #{response[:status]}" unless response[:status] == 200
189
+ raise "Size mismatch" unless response[:body].include?('"received_bytes":5')
190
+ puts " Sent 5 bytes, server received 5 bytes"
191
+ end
192
+
193
+ # === 12. 0-RTT Reconnection ===
194
+ puts "\n1️⃣2️⃣ 0-RTT Reconnection"
195
+ puts "-" * 60
196
+
197
+ test("Two separate connections show handshake cost") do
198
+ Quicsilver::Client.close_pool
199
+
200
+ # First connection — full handshake
201
+ client1 = Quicsilver::Client.new(HOST, PORT, unsecure: true)
202
+ t1 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
203
+ client1.get("/h3/ping")
204
+ first = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - t1) * 1000).round(1)
205
+ client1.disconnect
206
+
207
+ sleep 0.1
208
+
209
+ # Second connection — new handshake (0-RTT depends on MsQuic session cache)
210
+ client2 = Quicsilver::Client.new(HOST, PORT, unsecure: true)
211
+ t2 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
212
+ client2.get("/h3/ping")
213
+ second = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - t2) * 1000).round(1)
214
+ client2.disconnect
215
+
216
+ puts " First connection: #{first}ms"
217
+ puts " Second connection: #{second}ms"
218
+ puts " Pooling saves: ~#{first}ms per request by avoiding handshakes"
219
+ end
220
+
221
+ # === 13. GOAWAY Graceful Shutdown ===
222
+ puts "\n1️⃣3️⃣ GOAWAY (server keeps serving during test)"
223
+ puts "-" * 60
224
+
225
+ test("Server responds after many requests (no GOAWAY yet)") do
226
+ response = Quicsilver::Client.get(HOST, PORT, "/h3/ping", unsecure: true)
227
+ raise "Expected 200" unless response[:status] == 200
228
+ puts " Server still healthy after all tests"
229
+ end
230
+
231
+ # === 14. Rails CRUD ===
232
+ puts "\n1️⃣4️⃣ Rails CRUD"
233
+ puts "-" * 60
234
+
235
+ test("GET /posts.json returns posts array") do
236
+ response = Quicsilver::Client.get(HOST, PORT, "/posts.json", unsecure: true)
237
+ raise "Expected 200" unless response[:status] == 200
238
+ puts " Posts: #{response[:body][0..40]}"
239
+ end
240
+
241
+ # === 15. HTTP Methods ===
242
+ puts "\n1️⃣5️⃣ HTTP Methods"
243
+ puts "-" * 60
244
+
245
+ client = Quicsilver::Client.new(HOST, PORT, unsecure: true)
246
+ %i[get post put patch delete head].each do |method|
247
+ test("#{method.to_s.upcase} /h3/ping") do
248
+ response = client.public_send(method, "/h3/ping")
249
+ body_size = response[:body]&.bytesize || 0
250
+ puts " #{response[:status]} (#{body_size} bytes)"
251
+ end
252
+ end
253
+ client.disconnect
254
+
255
+ # === Summary ===
256
+ puts "\n" + "=" * 60
257
+ puts "🏁 Feature test complete!"
258
+ puts " Pool: #{Quicsilver::Client.pool.size} connection(s)"
259
+ Quicsilver::Client.close_pool
260
+ puts "👋 Done"
@@ -7,11 +7,11 @@ puts "🔌 Simple HTTP/3 Client Test"
7
7
  puts "=" * 40
8
8
 
9
9
  begin
10
- client = Quicsilver::Client.new("127.0.0.1", 4433, unsecure: true)
10
+ client = Quicsilver::Client.new("localhost", 4433, unsecure: true)
11
11
 
12
12
  client.connect
13
13
 
14
- response = client.get("/posts")
14
+ response = client.get("/")
15
15
 
16
16
  puts "Status: #{response[:status]}"
17
17
  puts "Headers: #{response[:headers].inspect}"
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Server-Sent Events over HTTP/3.
4
+ #
5
+ # Demonstrates streaming responses — data arrives chunk by chunk,
6
+ # not buffered. Each chunk is sent as a separate HTTP/3 DATA frame.
7
+ #
8
+ # ruby examples/streaming_sse.rb
9
+ # curl --http3-only -k https://localhost:4433/
10
+
11
+ require_relative "example_helper"
12
+
13
+ app = ->(env) {
14
+ body = Enumerator.new do |y|
15
+ 10.times do |i|
16
+ y << "data: {\"count\":#{i},\"time\":\"#{Time.now.iso8601}\"}\n\n"
17
+ sleep 0.2
18
+ end
19
+ y << "data: {\"done\":true}\n\n"
20
+ end
21
+
22
+ [200, { "content-type" => "text/event-stream", "cache-control" => "no-cache" }, body]
23
+ }
24
+
25
+ server = Quicsilver::Server.new(4433, app: app, server_configuration: EXAMPLE_TLS_CONFIG)
26
+
27
+ puts "📡 Streaming SSE over HTTP/3"
28
+ puts " https://localhost:4433"
29
+ puts " curl --http3-only -k https://localhost:4433/"
30
+ puts " Events arrive every 200ms — no buffering."
31
+ puts
32
+
33
+ server.start
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # HTTP/3 Trailers — send headers after the body.
4
+ #
5
+ # Trailers let the server report status after streaming is complete.
6
+ # Useful for checksums, streaming error status, and gRPC.
7
+ #
8
+ # ruby examples/trailers.rb
9
+
10
+ require_relative "example_helper"
11
+
12
+ PORT = 4433
13
+ HOST = "localhost"
14
+
15
+ app = ->(env) {
16
+ path = env["PATH_INFO"]
17
+
18
+ case path
19
+ when "/stream-with-checksum"
20
+ # Stream data, then send a checksum trailer
21
+ body = ["chunk1\n", "chunk2\n", "chunk3\n"]
22
+ checksum = Digest::SHA256.hexdigest(body.join)[0..7]
23
+
24
+ [200,
25
+ { "content-type" => "text/plain", "trailer" => "x-checksum" },
26
+ body]
27
+ # Note: trailers via Rack need the protocol-rack convention
28
+ # (see autoresearch.ideas.md for the Samuel Williams discussion)
29
+
30
+ when "/grpc-style"
31
+ # gRPC-style response: status comes in trailers, not headers
32
+ body = ['{"result":"processed"}']
33
+
34
+ [200,
35
+ { "content-type" => "application/json" },
36
+ body]
37
+
38
+ else
39
+ [200, { "content-type" => "text/plain" }, ["Try /stream-with-checksum or /grpc-style"]]
40
+ end
41
+ }
42
+
43
+ server = Quicsilver::Server.new(PORT, app: app, server_configuration: EXAMPLE_TLS_CONFIG)
44
+ server_thread = Thread.new { server.start }
45
+ sleep 0.3
46
+
47
+ puts "📎 HTTP/3 Trailers Demo"
48
+ puts "=" * 50
49
+
50
+ client = Quicsilver::Client.new(HOST, PORT, unsecure: true)
51
+
52
+ puts "\n Streaming with checksum trailer:"
53
+ response = client.get("/stream-with-checksum")
54
+ puts " Status: #{response[:status]}"
55
+ puts " Body: #{response[:body].inspect}"
56
+
57
+ puts "\n gRPC-style response:"
58
+ response = client.get("/grpc-style")
59
+ puts " Status: #{response[:status]}"
60
+ puts " Body: #{response[:body]}"
61
+
62
+ puts "\n Trailer support is built into the protocol layer."
63
+ puts " ResponseEncoder can send trailers after DATA frames."
64
+ puts " Full Rack integration needs protocol-rack update."
65
+
66
+ client.disconnect
67
+ server.stop
68
+ server_thread.join(2)
69
+ puts "\n✅ Done"
@@ -1,5 +1,19 @@
1
1
  require 'mkmf'
2
2
 
3
+ # Skip compilation if precompiled binary is already present
4
+ ext_dir = File.expand_path('../../lib/quicsilver', __dir__)
5
+ if File.exist?(File.join(ext_dir, 'quicsilver.bundle')) || File.exist?(File.join(ext_dir, 'quicsilver.so'))
6
+ File.write('Makefile', "install:\n\t@echo 'Using precompiled binary'\n\nall:\n\t@echo 'Using precompiled binary'\n")
7
+ exit
8
+ end
9
+
10
+ # On macOS, use Apple clang if available. Homebrew clang can't find
11
+ # system headers and produces broken binaries with MsQuic.
12
+ if RUBY_PLATFORM =~ /darwin/ && File.exist?("/usr/bin/clang")
13
+ RbConfig::CONFIG["CC"] = "/usr/bin/clang"
14
+ RbConfig::MAKEFILE_CONFIG["CC"] = "/usr/bin/clang"
15
+ end
16
+
3
17
  # Find MSQUIC in the submodule
4
18
  msquic_dir = File.expand_path('../../../vendor/msquic', __FILE__)
5
19