|
| 1 | +# Phase 3: MultiplexedOptions Implementation Plan |
| 2 | + |
| 3 | +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. |
| 4 | +
|
| 5 | +**Goal:** Create `MultiplexedOptions` — the shared configuration record for multiplexed protocols (HTTP/2 and HTTP/3). |
| 6 | + |
| 7 | +**Architecture:** A C# `record` with a static `Default` member and a `Validate()` method. Contains `SharedHttpOptions` (from H/1.0 Redesign) plus multiplexed-specific settings. Construction via `with`-expression. |
| 8 | + |
| 9 | +**Tech Stack:** .NET, xUnit v3 |
| 10 | + |
| 11 | +**Prerequisite:** Phase 2 complete. `SharedHttpOptions` must exist (from H/1.0 Redesign Phase 3). |
| 12 | + |
| 13 | +--- |
| 14 | + |
| 15 | +### Task 1: MultiplexedOptions Record |
| 16 | + |
| 17 | +**Files:** |
| 18 | +- Create: `src/TurboHTTP/Protocol/Multiplexed/Options/MultiplexedOptions.cs` |
| 19 | +- Test: `src/TurboHTTP.Tests/Multiplexed/MultiplexedOptionsSpec.cs` |
| 20 | + |
| 21 | +- [ ] **Step 1: Write the failing tests** |
| 22 | + |
| 23 | +```csharp |
| 24 | +// src/TurboHTTP.Tests/Multiplexed/MultiplexedOptionsSpec.cs |
| 25 | +using TurboHTTP.Protocol.Multiplexed.Options; |
| 26 | + |
| 27 | +namespace TurboHTTP.Tests.Multiplexed; |
| 28 | + |
| 29 | +public sealed class MultiplexedOptionsSpec |
| 30 | +{ |
| 31 | + [Fact(Timeout = 5000)] |
| 32 | + public void Default_should_have_100_max_concurrent_streams() |
| 33 | + { |
| 34 | + var options = MultiplexedOptions.Default; |
| 35 | + Assert.Equal(100, options.MaxConcurrentStreams); |
| 36 | + } |
| 37 | + |
| 38 | + [Fact(Timeout = 5000)] |
| 39 | + public void Default_should_have_16k_max_header_size() |
| 40 | + { |
| 41 | + var options = MultiplexedOptions.Default; |
| 42 | + Assert.Equal(16 * 1024, options.MaxHeaderSize); |
| 43 | + } |
| 44 | + |
| 45 | + [Fact(Timeout = 5000)] |
| 46 | + public void Default_should_have_64k_max_total_header_size() |
| 47 | + { |
| 48 | + var options = MultiplexedOptions.Default; |
| 49 | + Assert.Equal(64 * 1024, options.MaxTotalHeaderSize); |
| 50 | + } |
| 51 | + |
| 52 | + [Fact(Timeout = 5000)] |
| 53 | + public void Default_should_have_1s_reconnect_backoff() |
| 54 | + { |
| 55 | + var options = MultiplexedOptions.Default; |
| 56 | + Assert.Equal(TimeSpan.FromSeconds(1), options.ReconnectBackoff); |
| 57 | + } |
| 58 | + |
| 59 | + [Fact(Timeout = 5000)] |
| 60 | + public void Default_should_have_3_max_reconnect_attempts() |
| 61 | + { |
| 62 | + var options = MultiplexedOptions.Default; |
| 63 | + Assert.Equal(3, options.MaxReconnectAttempts); |
| 64 | + } |
| 65 | + |
| 66 | + [Fact(Timeout = 5000)] |
| 67 | + public void Validate_should_throw_when_MaxConcurrentStreams_is_zero() |
| 68 | + { |
| 69 | + var options = MultiplexedOptions.Default with { MaxConcurrentStreams = 0 }; |
| 70 | + var ex = Assert.Throws<ArgumentException>(() => options.Validate()); |
| 71 | + Assert.Contains("MaxConcurrentStreams", ex.Message); |
| 72 | + } |
| 73 | + |
| 74 | + [Fact(Timeout = 5000)] |
| 75 | + public void Validate_should_throw_when_MaxConcurrentStreams_is_negative() |
| 76 | + { |
| 77 | + var options = MultiplexedOptions.Default with { MaxConcurrentStreams = -1 }; |
| 78 | + var ex = Assert.Throws<ArgumentException>(() => options.Validate()); |
| 79 | + Assert.Contains("MaxConcurrentStreams", ex.Message); |
| 80 | + } |
| 81 | + |
| 82 | + [Fact(Timeout = 5000)] |
| 83 | + public void Validate_should_throw_when_MaxHeaderSize_is_zero() |
| 84 | + { |
| 85 | + var options = MultiplexedOptions.Default with { MaxHeaderSize = 0 }; |
| 86 | + var ex = Assert.Throws<ArgumentException>(() => options.Validate()); |
| 87 | + Assert.Contains("MaxHeaderSize", ex.Message); |
| 88 | + } |
| 89 | + |
| 90 | + [Fact(Timeout = 5000)] |
| 91 | + public void Validate_should_throw_when_MaxTotalHeaderSize_is_less_than_MaxHeaderSize() |
| 92 | + { |
| 93 | + var options = MultiplexedOptions.Default with |
| 94 | + { |
| 95 | + MaxHeaderSize = 1024, |
| 96 | + MaxTotalHeaderSize = 512 |
| 97 | + }; |
| 98 | + var ex = Assert.Throws<ArgumentException>(() => options.Validate()); |
| 99 | + Assert.Contains("MaxTotalHeaderSize", ex.Message); |
| 100 | + } |
| 101 | + |
| 102 | + [Fact(Timeout = 5000)] |
| 103 | + public void Validate_should_throw_when_ReconnectBackoff_is_negative() |
| 104 | + { |
| 105 | + var options = MultiplexedOptions.Default with { ReconnectBackoff = TimeSpan.FromSeconds(-1) }; |
| 106 | + var ex = Assert.Throws<ArgumentException>(() => options.Validate()); |
| 107 | + Assert.Contains("ReconnectBackoff", ex.Message); |
| 108 | + } |
| 109 | + |
| 110 | + [Fact(Timeout = 5000)] |
| 111 | + public void Validate_should_throw_when_MaxReconnectAttempts_is_negative() |
| 112 | + { |
| 113 | + var options = MultiplexedOptions.Default with { MaxReconnectAttempts = -1 }; |
| 114 | + var ex = Assert.Throws<ArgumentException>(() => options.Validate()); |
| 115 | + Assert.Contains("MaxReconnectAttempts", ex.Message); |
| 116 | + } |
| 117 | + |
| 118 | + [Fact(Timeout = 5000)] |
| 119 | + public void Validate_should_accept_zero_MaxReconnectAttempts() |
| 120 | + { |
| 121 | + var options = MultiplexedOptions.Default with { MaxReconnectAttempts = 0 }; |
| 122 | + options.Validate(); |
| 123 | + } |
| 124 | + |
| 125 | + [Fact(Timeout = 5000)] |
| 126 | + public void Validate_should_accept_zero_ReconnectBackoff() |
| 127 | + { |
| 128 | + var options = MultiplexedOptions.Default with { ReconnectBackoff = TimeSpan.Zero }; |
| 129 | + options.Validate(); |
| 130 | + } |
| 131 | + |
| 132 | + [Fact(Timeout = 5000)] |
| 133 | + public void Validate_should_pass_for_default() |
| 134 | + { |
| 135 | + MultiplexedOptions.Default.Validate(); |
| 136 | + } |
| 137 | + |
| 138 | + [Fact(Timeout = 5000)] |
| 139 | + public void With_expression_should_create_modified_copy() |
| 140 | + { |
| 141 | + var options = MultiplexedOptions.Default with { MaxConcurrentStreams = 50 }; |
| 142 | + Assert.Equal(50, options.MaxConcurrentStreams); |
| 143 | + Assert.Equal(100, MultiplexedOptions.Default.MaxConcurrentStreams); |
| 144 | + } |
| 145 | +} |
| 146 | +``` |
| 147 | + |
| 148 | +- [ ] **Step 2: Run tests to verify they fail** |
| 149 | + |
| 150 | +Run: `dotnet run --project src/TurboHTTP.Tests/TurboHTTP.Tests.csproj -- -class "TurboHTTP.Tests.Multiplexed.MultiplexedOptionsSpec"` |
| 151 | +Expected: Compilation error — `MultiplexedOptions` does not exist. |
| 152 | + |
| 153 | +- [ ] **Step 3: Implement MultiplexedOptions** |
| 154 | + |
| 155 | +```csharp |
| 156 | +// src/TurboHTTP/Protocol/Multiplexed/Options/MultiplexedOptions.cs |
| 157 | +namespace TurboHTTP.Protocol.Multiplexed.Options; |
| 158 | + |
| 159 | +public sealed record MultiplexedOptions |
| 160 | +{ |
| 161 | + public int MaxConcurrentStreams { get; init; } = 100; |
| 162 | + public int MaxHeaderSize { get; init; } = 16 * 1024; |
| 163 | + public int MaxTotalHeaderSize { get; init; } = 64 * 1024; |
| 164 | + public TimeSpan ReconnectBackoff { get; init; } = TimeSpan.FromSeconds(1); |
| 165 | + public int MaxReconnectAttempts { get; init; } = 3; |
| 166 | + |
| 167 | + public static MultiplexedOptions Default { get; } = new(); |
| 168 | + |
| 169 | + public void Validate() |
| 170 | + { |
| 171 | + if (MaxConcurrentStreams <= 0) |
| 172 | + { |
| 173 | + throw new ArgumentException("MaxConcurrentStreams must be greater than zero.", nameof(MaxConcurrentStreams)); |
| 174 | + } |
| 175 | + |
| 176 | + if (MaxHeaderSize <= 0) |
| 177 | + { |
| 178 | + throw new ArgumentException("MaxHeaderSize must be greater than zero.", nameof(MaxHeaderSize)); |
| 179 | + } |
| 180 | + |
| 181 | + if (MaxTotalHeaderSize < MaxHeaderSize) |
| 182 | + { |
| 183 | + throw new ArgumentException("MaxTotalHeaderSize must be greater than or equal to MaxHeaderSize.", nameof(MaxTotalHeaderSize)); |
| 184 | + } |
| 185 | + |
| 186 | + if (ReconnectBackoff < TimeSpan.Zero) |
| 187 | + { |
| 188 | + throw new ArgumentException("ReconnectBackoff must not be negative.", nameof(ReconnectBackoff)); |
| 189 | + } |
| 190 | + |
| 191 | + if (MaxReconnectAttempts < 0) |
| 192 | + { |
| 193 | + throw new ArgumentException("MaxReconnectAttempts must not be negative.", nameof(MaxReconnectAttempts)); |
| 194 | + } |
| 195 | + } |
| 196 | +} |
| 197 | +``` |
| 198 | + |
| 199 | +- [ ] **Step 4: Run tests to verify they pass** |
| 200 | + |
| 201 | +Run: `dotnet run --project src/TurboHTTP.Tests/TurboHTTP.Tests.csproj -- -class "TurboHTTP.Tests.Multiplexed.MultiplexedOptionsSpec"` |
| 202 | +Expected: All 15 tests PASS. |
| 203 | + |
| 204 | +- [ ] **Step 5: Commit** |
| 205 | + |
| 206 | +```bash |
| 207 | +git add src/TurboHTTP/Protocol/Multiplexed/Options/MultiplexedOptions.cs src/TurboHTTP.Tests/Multiplexed/MultiplexedOptionsSpec.cs |
| 208 | +git commit -m "feat(multiplexed): add MultiplexedOptions record with validation" |
| 209 | +``` |
| 210 | + |
| 211 | +--- |
| 212 | + |
| 213 | +### Task 2: Run full build and verify phase gate |
| 214 | + |
| 215 | +- [ ] **Step 1: Build the solution** |
| 216 | + |
| 217 | +Run: `dotnet build --configuration Release src/TurboHTTP.slnx` |
| 218 | +Expected: Build succeeds with zero errors. |
| 219 | + |
| 220 | +- [ ] **Step 2: Run all Multiplexed tests** |
| 221 | + |
| 222 | +Run: `dotnet run --project src/TurboHTTP.Tests/TurboHTTP.Tests.csproj -- -namespace "TurboHTTP.Tests.Multiplexed"` |
| 223 | +Expected: All specs PASS — StreamState, StreamTracker, CorrelationMap, ReconnectClassifier, ReconnectBuffer, MultiplexedOptions. |
| 224 | + |
| 225 | +- [ ] **Step 3: Run full test suite** |
| 226 | + |
| 227 | +Run: `dotnet test src/TurboHTTP.Tests/TurboHTTP.Tests.csproj` |
| 228 | +Expected: All tests PASS (existing + new). |
| 229 | + |
| 230 | +- [ ] **Step 4: Commit phase completion** |
| 231 | + |
| 232 | +```bash |
| 233 | +git commit --allow-empty -m "milestone: Phase 3 complete — Multiplexed layer fully implemented (Core, Encoding, Correlation, Reconnect, Options)" |
| 234 | +``` |
| 235 | + |
| 236 | +This concludes **Spec 1: Protocol/Multiplexed/**. The shared layer is ready for consumption by HTTP/2 (Phase 4–6) and HTTP/3 (Phase 7–9). |
0 commit comments