Enable WASM response streaming on audio media fetch

Without SetBrowserResponseStreamingEnabled the browser buffers the whole
body before yielding, so the Phase 21.2 read-loop pause backpressured an
already-downloaded payload. Set it on both the initial and seek/refill
requests; safe no-op on the SSR path.
This commit is contained in:
daniel-c-harvey
2026-06-24 08:45:33 -04:00
parent 9c95a5f23e
commit c7629c15a4
2 changed files with 93 additions and 0 deletions
@@ -0,0 +1,80 @@
using System.Net;
using DeepDrftModels.Enums;
using DeepDrftPublic.Client.Clients;
namespace DeepDrftTests;
/// <summary>
/// Pins the Phase 21.4 transport fix: every audio media fetch must carry the browser response-streaming
/// option so the body streams incrementally in WASM. Without it the browser buffers the whole payload
/// before the response stream yields a byte, and the 21.2 read-loop pause backpressures nothing.
///
/// The flag is set by <c>SetBrowserResponseStreamingEnabled(true)</c>, which records it in
/// <see cref="HttpRequestMessage.Options"/> under the framework key <c>"WebAssemblyEnableStreamingResponse"</c>.
/// A stub handler reads that option back during <c>SendAsync</c> — the same network-boundary fake the Opus
/// format-selection tests use. True network backpressure is browser-only and cannot be unit-profiled; this
/// asserts the request is *configured* for streaming, which is the part the harness can observe. Daniel's
/// 21.4 manual re-run confirms the actual bounded-memory behaviour.
///
/// Both the initial full-stream request (byteOffset 0) and the seek/refill Range request (byteOffset &gt; 0,
/// Phase 21.3) flow through <see cref="TrackMediaClient.GetTrackMedia"/>, so both are asserted here.
/// </summary>
[TestFixture]
public class TrackMediaStreamingEnabledTests
{
// The framework key SetBrowserResponseStreamingEnabled writes into HttpRequestMessage.Options.
private static readonly HttpRequestOptionsKey<bool> StreamingOptionKey = new("WebAssemblyEnableStreamingResponse");
// Captures the streaming option off each outgoing request, then returns a minimal 200 with a body so
// GetTrackMedia reaches its pass path (ReadAsStreamAsync over ByteArrayContent).
private sealed class CapturingHandler : HttpMessageHandler
{
public bool? StreamingEnabled { get; private set; }
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
StreamingEnabled = request.Options.TryGetValue(StreamingOptionKey, out var enabled) ? enabled : null;
var response = new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new ByteArrayContent("audio-bytes"u8.ToArray()),
};
return Task.FromResult(response);
}
}
private sealed class SingleClientFactory : IHttpClientFactory
{
private readonly HttpMessageHandler _handler;
public SingleClientFactory(HttpMessageHandler handler) => _handler = handler;
public HttpClient CreateClient(string name) =>
new(_handler, disposeHandler: false) { BaseAddress = new Uri("https://content.test/") };
}
[Test]
public async Task GetTrackMedia_InitialStream_EnablesBrowserResponseStreaming()
{
var handler = new CapturingHandler();
var client = new TrackMediaClient(new SingleClientFactory(handler));
var result = await client.GetTrackMedia("track-1", byteOffset: 0, format: AudioFormat.Lossless);
Assert.That(result.Success, Is.True, "the fetch should succeed against the stub");
Assert.That(handler.StreamingEnabled, Is.True,
"the initial media stream must enable browser response streaming or the read-loop pause backpressures nothing");
}
[Test]
public async Task GetTrackMedia_SeekOffsetStream_EnablesBrowserResponseStreaming()
{
var handler = new CapturingHandler();
var client = new TrackMediaClient(new SingleClientFactory(handler));
var result = await client.GetTrackMedia("track-1", byteOffset: 1_048_576, format: AudioFormat.Opus);
Assert.That(result.Success, Is.True, "the offset fetch should succeed against the stub");
Assert.That(handler.StreamingEnabled, Is.True,
"the seek/refill Range request must also enable streaming — 21.3 refill depends on the same backpressure");
}
}