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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: eb40bd31dbb1c684ae68ec9b8a5f9fd0caee6972d360dc2be67984e473fdd15a
4
- data.tar.gz: 2f46305042b7253d9fedc0018947404d6f27b045aef43be7a03ec32d3fe0bb8d
3
+ metadata.gz: d4926d24fbce141791505bb14d59e543a30a6c5cbf8ca3c3fd36c9123b2941db
4
+ data.tar.gz: 45c128c6bf9fc48b8267ba2927414f8429607e8f99e4169235d12ebe26863ccc
5
5
  SHA512:
6
- metadata.gz: 48810a1f5a78021b027cb416bca181388b23889456e61c4c05702bd228f15da52026c25b2eedf375473054133300c62efd88c486367ddb987490676c8575a302
7
- data.tar.gz: 7ebe3bdb83efaf1854655f6553e324ee4c425c4b2d202e76cb2c2adce0bfd814e567003fb2b315cb6b373acf8cfed8fb7f08dffa3859e4e103e3480d4877d8c6
6
+ metadata.gz: ad6f92657375909bac67ce88bfa64d47c53a2dd69bb424a36f297927778389247714c092b004287cc278c5ecdf94c0f186a4c15f4fa4cfff62dbef9fcfc155f6
7
+ data.tar.gz: 1f5d2812bfb18a15b1821c8afcb18f779663ec3066495ef69dfd9a1c79f8d83f13f7c6287b8ce56be318476ea69fd6c520c1dc4298ceb807c64f6de0747b3035
@@ -22,15 +22,14 @@ jobs:
22
22
  runs-on: ubuntu-24.04
23
23
  strategy:
24
24
  matrix:
25
- ruby-version: ["3.2"]
25
+ ruby-version: ["3.4"]
26
26
 
27
27
  steps:
28
28
  - uses: actions/checkout@v4
29
+ with:
30
+ submodules: recursive
29
31
  - name: Set up Ruby
30
- # To automatically get bug fixes and new Ruby versions for ruby/setup-ruby,
31
- # change this to (see https://github.com/ruby/setup-ruby#versioning):
32
- # uses: ruby/setup-ruby@v1
33
- uses: ruby/setup-ruby@55283cc23133118229fd3f97f9336ee23a179fcf # v1.146.0
32
+ uses: ruby/setup-ruby@v1
34
33
  with:
35
34
  ruby-version: ${{ matrix.ruby-version }}
36
35
  bundler-cache: true # runs 'bundle install' and caches installed gems automatically
@@ -0,0 +1,93 @@
1
+ name: "Package and release gems with precompiled binaries"
2
+ on:
3
+ workflow_dispatch:
4
+ inputs:
5
+ release:
6
+ description: "If the whole build passes on all platforms, release the gems on RubyGems.org"
7
+ required: false
8
+ type: boolean
9
+ default: false
10
+ env:
11
+ CIBUILDGEM: 1
12
+ jobs:
13
+ compile:
14
+ timeout-minutes: 20
15
+ name: "Cross compile the gem on different ruby versions"
16
+ strategy:
17
+ matrix:
18
+ os: ["macos-latest", "ubuntu-22.04"]
19
+ runs-on: "${{ matrix.os }}"
20
+ steps:
21
+ - name: "Checkout code"
22
+ uses: "actions/checkout@v5"
23
+ with:
24
+ submodules: recursive
25
+ - name: "Setup Ruby"
26
+ uses: "ruby/setup-ruby@v1"
27
+ with:
28
+ ruby-version: "3.4"
29
+ bundler-cache: true
30
+ - name: "Build MsQuic"
31
+ run: bundle exec rake build_msquic
32
+ - name: "Run cibuildgem"
33
+ uses: "shopify/cibuildgem/.github/actions/cibuildgem@main"
34
+ with:
35
+ step: "compile"
36
+ test:
37
+ timeout-minutes: 20
38
+ name: "Run the test suite"
39
+ needs: compile
40
+ strategy:
41
+ matrix:
42
+ os: ["macos-latest", "ubuntu-22.04"]
43
+ rubies: ["3.4", "4.0"]
44
+ type: ["cross", "native"]
45
+ runs-on: "${{ matrix.os }}"
46
+ steps:
47
+ - name: "Checkout code"
48
+ uses: "actions/checkout@v5"
49
+ - name: "Setup Ruby"
50
+ uses: "ruby/setup-ruby@v1"
51
+ with:
52
+ ruby-version: "${{ matrix.rubies }}"
53
+ bundler-cache: true
54
+ - name: "Run cibuildgem"
55
+ uses: "shopify/cibuildgem/.github/actions/cibuildgem@main"
56
+ with:
57
+ step: "test_${{ matrix.type }}"
58
+ install:
59
+ timeout-minutes: 5
60
+ name: "Verify the gem can be installed"
61
+ needs: test
62
+ strategy:
63
+ matrix:
64
+ os: ["macos-latest", "ubuntu-22.04"]
65
+ runs-on: "${{ matrix.os }}"
66
+ steps:
67
+ - name: "Setup Ruby"
68
+ uses: "ruby/setup-ruby@v1"
69
+ with:
70
+ ruby-version: "4.0.0"
71
+ - name: "Run cibuildgem"
72
+ uses: "shopify/cibuildgem/.github/actions/cibuildgem@main"
73
+ with:
74
+ step: "install"
75
+ release:
76
+ environment: release
77
+ permissions:
78
+ id-token: write
79
+ contents: read
80
+ timeout-minutes: 5
81
+ if: ${{ inputs.release }}
82
+ name: "Release all gems with RubyGems"
83
+ needs: install
84
+ runs-on: "ubuntu-latest"
85
+ steps:
86
+ - name: "Setup Ruby"
87
+ uses: "ruby/setup-ruby@v1"
88
+ with:
89
+ ruby-version: "4.0.0"
90
+ - name: "Run cibuildgem"
91
+ uses: "shopify/cibuildgem/.github/actions/cibuildgem@main"
92
+ with:
93
+ step: "release"
data/.gitignore CHANGED
@@ -11,4 +11,6 @@
11
11
  /certificates/*
12
12
  *.bundle
13
13
  lib/quicsilver/quicsilver.bundle
14
- *.gem
14
+ *.gem
15
+ autoresearch.*
16
+ ext/quicsilver/Makefile
data/CHANGELOG.md CHANGED
@@ -5,6 +5,87 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.4.0] - 2026-04-25
9
+
10
+ ### Added
11
+ - Client connection pool with automatic reuse (`Quicsilver::Client.get/post` class-level API)
12
+ - GREASE support (RFC 9297) — settings, frames, and unidirectional streams
13
+ - GOAWAY validation (RFC 9114 §7.2.6) — monotonically decreasing IDs, stream ID validation
14
+ - Trailer support (RFC 9114 §4.1) — parse and send trailing HEADERS frames
15
+ - Extensible Priorities (RFC 9218) — parse `priority` header, PRIORITY_UPDATE frames on control stream, MsQuic stream priority mapping
16
+ - FrameParser base class — unified frame walking, ordering, body accumulation, size limits
17
+ - FrameReader module — shared byte-level frame extraction for request/response/control streams
18
+ - Trailer wiring in Adapter and StreamOutput for protocol-http integration
19
+ - Informational 1xx responses (§4.1) — 103 Early Hints with `rack.early_hints` support for Rails
20
+ - Two-phase GOAWAY shutdown (§5.2) — server sends decreasing GOAWAY IDs during graceful shutdown
21
+ - Client processes server SETTINGS (§7.2.4) — parses peer's SETTINGS including MAX_FIELD_SECTION_SIZE
22
+ - Client processes server GOAWAY (§5.2) — tracks peer_goaway_id, blocks new requests, connection pool evicts draining connections
23
+ - MIT license in gemspec
24
+
25
+ ### Fixed
26
+ - QPACK prefix decoding — decode Required Insert Count and Delta Base as varints instead of hardcoded `offset = 2`
27
+ - Default decoder rejects payloads referencing the dynamic table
28
+ - Response parser now enforces `max_frame_payload_size` (was missing)
29
+ - Duplicate `frames` method in FrameParser
30
+ - Consistent `@headers` and `@trailers` initialization (`{}` not `nil`)
31
+ - extconf.rb — force Apple clang on macOS (Homebrew clang produces broken MsQuic binaries)
32
+
33
+ ### Changed
34
+ - RequestParser and ResponseParser inherit from FrameParser (reduced ~230 lines of duplication)
35
+ - `store_header`, `body`, `DEFAULT_DECODER`, `EMPTY_BODY`, `parse!` moved to FrameParser base class
36
+ - `@body_io` renamed to `@body` in ResponseParser for consistency
37
+ - ResponseEncoder accepts optional `trailers:` hash
38
+ - StreamOutput accepts `send_fin:` parameter for trailer support
39
+
40
+ ## [0.3.0] - 2026-03-23
41
+
42
+ ### Added
43
+ - QPACK Huffman coding with 8-bit decode table and encode/decode caching
44
+ - 0-RTT replay protection for unsafe HTTP methods
45
+ - Bounded backpressure support
46
+ - Buffer size limits to prevent memory exhaustion (configurable `max_body_size`, `max_header_size`, `max_header_count`, `max_frame_payload_size`)
47
+ - Content-length validation
48
+ - Multi-value header support for duplicate header fields
49
+ - Headers validation: reject connection-specific headers, require `:authority` or `host` for http/https schemes
50
+ - Incremental unidirectional stream processing with critical stream protection
51
+ - QPACK encoder and decoder stream instruction validation
52
+ - Spec-correct error signaling with error codes on `FrameError` and `MessageError`
53
+ - Suppress response body for HEAD requests per RFC 9114 §4.1
54
+ - Allow `te: trailers` header in requests per RFC 9114 §4.2
55
+ - Custom ALPN support (no longer hardcoded to `h3`)
56
+ - `Stream` and `StreamEvent` abstractions to encapsulate C extension details
57
+ - Dual-stack (IPv4/IPv6) listener support — fixes TLS handshake failures on macOS
58
+ - Client `PUT` method
59
+ - Integration test suite for curl HTTP/3
60
+
61
+ ### Fixed
62
+ - Memory leaks: free `StreamContext` on `SHUTDOWN_COMPLETE`, free `ConnectionContext` on `CONNECTION_SHUTDOWN_COMPLETE`, close `EventQ`/`ExecContext`/`WakeFd` on shutdown
63
+ - Double-free and handle leaks in C extension
64
+ - `dispatch_to_ruby` safety with `rb_protect`; client use-after-free fix
65
+ - Infinite loop on truncated varint in request/response parsers
66
+ - Frame ordering: `DATA` before `HEADERS` now raises `FrameError`
67
+ - `STOP_SENDING` / `STREAM_RESET` compliance — server properly cancels streams and resets send side
68
+ - Control stream validation: reject duplicate settings, forbidden frame types, and reserved HTTP/2 types
69
+ - QPACK static table index 57/58 casing (`includeSubDomains`)
70
+ - Stale stream handle guard in cancel and C extension
71
+ - Replaced `Thread.kill` with `Thread.raise(DrainTimeoutError)` for clean drain
72
+ - Binary encoding for `buffer_data` and empty FIN handling
73
+ - Linux/GitHub CI: use epoll instead of kqueue on non-Darwin platforms
74
+ - Circular require warning
75
+
76
+ ### Changed
77
+ - Reorganized gem structure: `protocol/`, `server/`, `transport/` directories
78
+ - Server owns the 0-RTT policy
79
+ - QPACK encoder uses O(1) static table lookup with multi-level caching
80
+ - QPACK decoder uses string-based decoding with result caching
81
+ - HTTP/3 parsers optimized with parse-level caching and lazy allocation
82
+ - Varint encoding/decoding optimized with precomputed tables
83
+ - HTTP/3 encoders handle framing only; QPACK handles field encoding (cleaner separation)
84
+ - MsQuic custom execution mode with configurable worker pool and throughput settings
85
+
86
+ ### Limitations
87
+ - Client does not reuse connections
88
+
8
89
  ## [0.2.0] - 2025-12-17
9
90
 
10
91
  ### Added
data/Gemfile.lock CHANGED
@@ -1,22 +1,43 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- quicsilver (0.1.0)
4
+ quicsilver (0.4.0)
5
+ console
5
6
  localhost (~> 1.6)
7
+ logger
8
+ protocol-http (~> 0.49)
9
+ protocol-rack (~> 0.22)
6
10
  rack (~> 3.0)
7
11
  rackup (~> 2.0)
8
12
 
9
13
  GEM
10
14
  remote: https://rubygems.org/
11
15
  specs:
16
+ benchmark-ips (2.14.0)
17
+ console (1.34.3)
18
+ fiber-annotation
19
+ fiber-local (~> 1.1)
20
+ json
21
+ fiber-annotation (0.2.0)
22
+ fiber-local (1.1.0)
23
+ fiber-storage
24
+ fiber-storage (1.0.1)
25
+ io-stream (0.11.1)
26
+ json (2.15.2)
12
27
  localhost (1.6.0)
13
- minitest (5.25.5)
28
+ logger (1.7.0)
29
+ minitest (5.27.0)
14
30
  minitest-focus (1.4.0)
15
31
  minitest (>= 4, < 6)
32
+ protocol-http (0.60.0)
33
+ protocol-rack (0.22.1)
34
+ io-stream (>= 0.10)
35
+ protocol-http (~> 0.58)
36
+ rack (>= 1.0)
16
37
  rack (3.2.4)
17
38
  rackup (2.2.1)
18
39
  rack (>= 3)
19
- rake (10.5.0)
40
+ rake (13.4.2)
20
41
  rake-compiler (1.3.0)
21
42
  rake
22
43
  rake-compiler-dock (1.9.1)
@@ -26,11 +47,12 @@ PLATFORMS
26
47
  ruby
27
48
 
28
49
  DEPENDENCIES
50
+ benchmark-ips (~> 2.12)
29
51
  bundler (~> 2.0)
30
52
  minitest (~> 5.0)
31
53
  minitest-focus (~> 1.3)
32
54
  quicsilver!
33
- rake (~> 10.0)
55
+ rake (~> 13.0)
34
56
  rake-compiler (~> 1.2)
35
57
  rake-compiler-dock (~> 1.3)
36
58
 
data/README.md CHANGED
@@ -1,13 +1,25 @@
1
1
  # Quicsilver
2
2
 
3
- HTTP/3 server for Ruby with Rack support.
4
-
5
- **Status:** Experimental (v0.2.0)
3
+ HTTP/3 server and client for Ruby with Rack support.
4
+
5
+ ## Features
6
+
7
+ - **HTTP/3 server** — serve any Rack app over QUIC/HTTP/3
8
+ - **HTTP/3 client** — make requests with automatic connection pooling
9
+ - **Rack integration** — `rackup -s quicsilver` works with Rails, Sinatra, any Rack app
10
+ - **Streaming** — dispatch on HEADERS, stream body chunks as they arrive
11
+ - **Extensible Priorities** (RFC 9218) — CSS before images, server respects client priority hints
12
+ - **Trailers** (RFC 9114 §4.1) — send/receive trailing headers after the body
13
+ - **GREASE** (RFC 9297) — extensibility testing on settings, frames, and streams
14
+ - **GOAWAY** (RFC 9114 §7.2.6) — graceful connection draining with validation
15
+ - **0-RTT** — fast reconnection with replay protection
16
+ - **Connection pooling** — client reuses connections automatically
17
+ - **protocol-http integration** — works with Falcon and protocol-http ecosystem
6
18
 
7
19
  ## Installation
8
20
 
9
21
  ```bash
10
- git clone <repository>
22
+ git clone https://github.com/hahmed/quicsilver
11
23
  cd quicsilver
12
24
  bundle install
13
25
  rake compile
@@ -21,18 +33,11 @@ rake compile
21
33
  require "quicsilver"
22
34
 
23
35
  app = ->(env) {
24
- case env['PATH_INFO']
25
- when '/'
26
- [200, {'content-type' => 'text/plain'}, ["Hello HTTP/3!"]]
27
- when '/api/users'
28
- [200, {'content-type' => 'application/json'}, ['{"users": ["alice", "bob"]}']]
29
- else
30
- [404, {'content-type' => 'text/plain'}, ["Not Found"]]
31
- end
36
+ [200, {"content-type" => "text/plain"}, ["Hello HTTP/3!"]]
32
37
  }
33
38
 
34
39
  server = Quicsilver::Server.new(4433, app: app)
35
- server.start # Blocks until shutdown
40
+ server.start
36
41
  ```
37
42
 
38
43
  ### Client
@@ -40,43 +45,102 @@ server.start # Blocks until shutdown
40
45
  ```ruby
41
46
  require "quicsilver"
42
47
 
43
- client = Quicsilver::Client.new("127.0.0.1", 4433, unsecure: true)
44
- client.connect
45
-
46
- response = client.get("/api/users")
47
- puts response[:body]
48
+ # Class-level API with automatic connection pooling
49
+ response = Quicsilver::Client.get("127.0.0.1", 4433, "/")
50
+ puts response[:status] # => 200
51
+ puts response[:body] # => "Hello HTTP/3!"
48
52
 
49
- response = client.post("/api/users", body: '{"name": "charlie"}')
53
+ # POST with body
54
+ response = Quicsilver::Client.post("127.0.0.1", 4433, "/api/users",
55
+ body: '{"name": "alice"}',
56
+ headers: { "content-type" => "application/json" })
50
57
 
58
+ # Instance-level for more control
59
+ client = Quicsilver::Client.new("127.0.0.1", 4433, unsecure: true)
60
+ response = client.get("/")
51
61
  client.disconnect
52
62
  ```
53
63
 
54
- ## Usage with Rails
64
+ ### Rails
55
65
 
56
66
  ```bash
57
67
  rackup -s quicsilver -p 4433
58
68
  ```
59
69
 
70
+ ### curl
71
+
72
+ ```bash
73
+ curl --http3-only https://localhost:4433/
74
+ ```
75
+
60
76
  ## Configuration
61
77
 
62
78
  ```ruby
63
- server = Quicsilver::Server.new(4433,
64
- app: app,
65
- address: "0.0.0.0",
66
- idle_timeout: 10_000, # Connection idle timeout (ms)
67
- initial_window_size: 65536, # Flow control window
68
- max_streams_bidi: 100, # Max concurrent requests
69
- cert_path: "/path/to/cert.pem", # TLS certificate
70
- key_path: "/path/to/key.pem" # TLS private key
79
+ config = Quicsilver::Transport::Configuration.new(
80
+ "certificates/server.crt",
81
+ "certificates/server.key",
82
+ idle_timeout_ms: 10_000,
83
+ max_concurrent_requests: 100,
84
+ max_body_size: 10 * 1024 * 1024, # 10MB body limit (optional)
85
+ max_header_size: 64 * 1024, # 64KB header limit (optional)
86
+ max_header_count: 128, # Header count limit (optional)
87
+ stream_receive_window: 262_144, # 256KB per stream
88
+ connection_flow_control_window: 16_777_216 # 16MB per connection
89
+ )
90
+
91
+ server = Quicsilver::Server.new(4433, app: app, server_configuration: config)
92
+ server.start
93
+ ```
94
+
95
+ ## Priorities
96
+
97
+ Browsers send priority hints on requests. Quicsilver parses them and tells MsQuic to schedule high-priority streams first.
98
+
99
+ ```
100
+ GET /style.css → priority: u=0 → sent first (highest urgency)
101
+ GET /app.js → priority: u=1 → sent second
102
+ GET /hero.png → priority: u=5 → sent later
103
+ ```
104
+
105
+ No configuration needed — it works automatically.
106
+
107
+ ## Trailers
108
+
109
+ Send headers after the body — useful for checksums, streaming status, and gRPC.
110
+
111
+ ```ruby
112
+ # Trailers work with protocol-http's Headers#trailer! API
113
+ headers = Protocol::HTTP::Headers.new
114
+ headers.add("content-type", "text/plain")
115
+ headers.trailer!
116
+ headers.add("x-checksum", "abc123")
117
+ ```
118
+
119
+ ## Protocol-HTTP Mode
120
+
121
+ For integration with [Falcon](https://github.com/socketry/falcon) and the protocol-http ecosystem:
122
+
123
+ ```ruby
124
+ config = Quicsilver::Transport::Configuration.new(
125
+ "certificates/server.crt",
126
+ "certificates/server.key",
127
+ mode: :protocol_http
71
128
  )
129
+
130
+ server = Quicsilver::Server.new(4433, app: app, server_configuration: config)
131
+ server.start
72
132
  ```
73
133
 
134
+ | Mode | Body Handling | Use Case |
135
+ |------|---------------|----------|
136
+ | `:rack` (default) | Buffered | Standard Rack apps |
137
+ | `:protocol_http` | Streaming | Falcon, protocol-http apps |
138
+
74
139
  ## Development
75
140
 
76
141
  ```bash
77
- rake compile # Build C extension
78
- rake test # Run tests (122 passing)
79
- rake clean # Clean build artifacts
142
+ rake compile # Build C extension (macOS: uses Apple clang automatically)
143
+ rake test # Run tests
80
144
  ```
81
145
 
82
146
  ## License
data/Rakefile CHANGED
@@ -1,3 +1,4 @@
1
+ require "bundler/setup"
1
2
  require "bundler/gem_tasks"
2
3
  require "rake/testtask"
3
4
  require "rake/extensiontask"
@@ -6,6 +7,27 @@ Rake::ExtensionTask.new('quicsilver') do |ext|
6
7
  ext.lib_dir = 'lib/quicsilver'
7
8
  end
8
9
 
10
+ # Ensure MsQuic is built before compiling the C extension
11
+ task :compile => :build_msquic
12
+
13
+ # Copy MsQuic dylib next to the compiled extension for gem packaging
14
+ task :bundle_msquic do
15
+ lib_dir = 'lib/quicsilver'
16
+ if RUBY_PLATFORM =~ /darwin/
17
+ dylib = 'vendor/msquic/build/bin/Release/libmsquic.2.dylib'
18
+ if File.exist?(dylib)
19
+ cp dylib, "#{lib_dir}/libmsquic.2.dylib"
20
+ # Update rpath so the bundle finds the dylib in the same directory
21
+ sh "install_name_tool -add_rpath @loader_path #{lib_dir}/quicsilver.bundle 2>/dev/null || true"
22
+ end
23
+ elsif RUBY_PLATFORM =~ /linux/
24
+ so = 'vendor/msquic/build/bin/Release/libmsquic.so.2'
25
+ cp so, "#{lib_dir}/libmsquic.so.2" if File.exist?(so)
26
+ end
27
+ end
28
+
29
+ task :build => :bundle_msquic
30
+
9
31
  task :setup do
10
32
  # Initialize git submodule if it doesn't exist
11
33
  unless File.exist?('vendor/msquic')
@@ -15,9 +37,18 @@ task :setup do
15
37
  end
16
38
 
17
39
  task :build_msquic => :setup do
18
- # Build MSQUIC using CMake with proper macOS framework linking
19
- sh 'cd vendor/msquic && cmake -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_EXE_LINKER_FLAGS="-framework CoreServices" -DCMAKE_SHARED_LINKER_FLAGS="-framework CoreServices"'
20
- sh 'cd vendor/msquic && cmake --build build --config Release'
40
+ cmake_args = ['-B build', '-DCMAKE_BUILD_TYPE=Release']
41
+ if RUBY_PLATFORM =~ /darwin/
42
+ cmake_args << '-DCMAKE_EXE_LINKER_FLAGS="-framework CoreServices"'
43
+ cmake_args << '-DCMAKE_SHARED_LINKER_FLAGS="-framework CoreServices"'
44
+ # Ensure QuicTLS uses Xcode SDK, not Homebrew OpenSSL
45
+ sdk_path = `xcrun --show-sdk-path 2>/dev/null`.strip
46
+ cmake_args << "-DCMAKE_OSX_SYSROOT=#{sdk_path}" unless sdk_path.empty?
47
+ end
48
+ # Override PATH so QuicTLS openssldir detection finds system openssl, not Homebrew
49
+ env = { 'PATH' => "/usr/bin:#{ENV['PATH']}" }
50
+ sh env, "cd vendor/msquic && cmake #{cmake_args.join(' ')}"
51
+ sh env, 'cd vendor/msquic && cmake --build build --config Release'
21
52
  end
22
53
 
23
54
  task :build => [:build_msquic, :compile]
@@ -28,4 +59,65 @@ Rake::TestTask.new(:test) do |t|
28
59
  t.test_files = FileList["test/**/*_test.rb"]
29
60
  end
30
61
 
62
+ Rake::TestTask.new(:test_unit) do |t|
63
+ t.libs << "test"
64
+ t.libs << "lib"
65
+ t.test_files = FileList["test/**/*_test.rb"].reject { |f|
66
+ f.include?("integration") || f.include?("stream_control") ||
67
+ f =~ /quicsilver_test|event_loop_test/
68
+ }
69
+ end
70
+
71
+ Rake::TestTask.new(:test_integration) do |t|
72
+ t.libs << "test"
73
+ t.libs << "lib"
74
+ t.test_files = FileList[
75
+ "test/stream_control_integration_test.rb",
76
+ "test/integration/**/*_test.rb",
77
+ "test/quicsilver_test.rb",
78
+ "test/event_loop_test.rb"
79
+ ]
80
+ end
81
+
82
+ desc "Run unit and integration tests in parallel"
83
+ task :test_parallel do
84
+ threads = []
85
+ results = {}
86
+
87
+ threads << Thread.new {
88
+ results[:unit] = system("bundle exec rake test_unit 2>&1 > /dev/null")
89
+ }
90
+ threads << Thread.new {
91
+ results[:integration] = system("bundle exec rake test_integration 2>&1 > /dev/null")
92
+ }
93
+
94
+ threads.each(&:join)
95
+ unless results.values.all?
96
+ abort "Tests failed: #{results.inspect}"
97
+ end
98
+ end
99
+
100
+ namespace :benchmark do
101
+ desc "Run throughput benchmark"
102
+ task :throughput do
103
+ ruby "benchmarks/throughput.rb"
104
+ end
105
+
106
+ desc "Run concurrency benchmark"
107
+ task :concurrent do
108
+ ruby "benchmarks/concurrent.rb"
109
+ end
110
+
111
+ desc "Run component micro-benchmarks"
112
+ task :components do
113
+ ruby "benchmarks/components.rb"
114
+ end
115
+
116
+ desc "Run all benchmarks"
117
+ task :all => [:components, :throughput, :concurrent]
118
+ end
119
+
120
+ desc "Run all benchmarks"
121
+ task :benchmark => "benchmark:all"
122
+
31
123
  task :default => :test