diff --git a/src/Components/Server/src/Circuits/RemoteJSDataStream.cs b/src/Components/Server/src/Circuits/RemoteJSDataStream.cs index 807f18402401..7c3f27853f43 100644 --- a/src/Components/Server/src/Circuits/RemoteJSDataStream.cs +++ b/src/Components/Server/src/Circuits/RemoteJSDataStream.cs @@ -40,9 +40,10 @@ public static async ValueTask CreateRemoteJSDataStreamAsync( RemoteJSRuntime runtime, IJSStreamReference jsStreamReference, long totalLength, - long maxBufferSize, long maximumIncomingBytes, TimeSpan jsInteropDefaultCallTimeout, + long pauseIncomingBytesThreshold = -1, + long resumeIncomingBytesThreshold = -1, CancellationToken cancellationToken = default) { // Enforce minimum 1 kb, maximum 50 kb, SignalR message size. @@ -54,7 +55,7 @@ public static async ValueTask CreateRemoteJSDataStreamAsync( throw new ArgumentException($"SignalR MaximumIncomingBytes must be at least 1 kb."); var streamId = runtime.RemoteJSDataStreamNextInstanceId++; - var remoteJSDataStream = new RemoteJSDataStream(runtime, streamId, totalLength, maxBufferSize, jsInteropDefaultCallTimeout, cancellationToken); + var remoteJSDataStream = new RemoteJSDataStream(runtime, streamId, totalLength, jsInteropDefaultCallTimeout, pauseIncomingBytesThreshold, resumeIncomingBytesThreshold, cancellationToken); await runtime.InvokeVoidAsync("Blazor._internal.sendJSDataStream", jsStreamReference, streamId, chunkSize); return remoteJSDataStream; } @@ -63,8 +64,9 @@ private RemoteJSDataStream( RemoteJSRuntime runtime, long streamId, long totalLength, - long maxBufferSize, TimeSpan jsInteropDefaultCallTimeout, + long pauseIncomingBytesThreshold, + long resumeIncomingBytesThreshold, CancellationToken cancellationToken) { _runtime = runtime; @@ -78,10 +80,16 @@ private RemoteJSDataStream( _runtime.RemoteJSDataStreamInstances.Add(_streamId, this); - _pipe = new Pipe(new PipeOptions(pauseWriterThreshold: maxBufferSize, resumeWriterThreshold: maxBufferSize / 2)); + _pipe = new Pipe(new PipeOptions(pauseWriterThreshold: pauseIncomingBytesThreshold, resumeWriterThreshold: resumeIncomingBytesThreshold)); _pipeReaderStream = _pipe.Reader.AsStream(); + PipeReader = _pipe.Reader; } + /// + /// Gets a to directly read data sent by the JavaScript client. + /// + public PipeReader PipeReader { get; } + private async Task ReceiveData(long chunkId, byte[] chunk, string error) { try @@ -199,13 +207,23 @@ private async Task ThrowOnTimeout() if (!_disposed && (DateTimeOffset.UtcNow >= _lastDataReceivedTime.Add(_jsInteropDefaultCallTimeout))) { // Dispose of the stream if a chunk isn't received within the jsInteropDefaultCallTimeout. - var timeoutException = new TimeoutException("Did not receive any data in the alloted time."); + var timeoutException = new TimeoutException("Did not receive any data in the allotted time."); await CompletePipeAndDisposeStream(timeoutException); _runtime.RaiseUnhandledException(timeoutException); } } - internal async Task CompletePipeAndDisposeStream(Exception? ex = null) + /// + /// For testing purposes only. + /// + /// Triggers the timeout on the next check. + /// + internal void InvalidateLastDataReceivedTimeForTimeout() + { + _lastDataReceivedTime = _lastDataReceivedTime.Subtract(_jsInteropDefaultCallTimeout); + } + + private async Task CompletePipeAndDisposeStream(Exception? ex = null) { await _pipe.Writer.CompleteAsync(ex); Dispose(true); diff --git a/src/Components/Server/src/Circuits/RemoteJSRuntime.cs b/src/Components/Server/src/Circuits/RemoteJSRuntime.cs index 757359f1f926..f51fefb27ff1 100644 --- a/src/Components/Server/src/Circuits/RemoteJSRuntime.cs +++ b/src/Components/Server/src/Circuits/RemoteJSRuntime.cs @@ -157,8 +157,8 @@ public void MarkPermanentlyDisconnected() _clientProxy = null; } - protected override async Task ReadJSDataAsStreamAsync(IJSStreamReference jsStreamReference, long totalLength, long maxBufferSize, CancellationToken cancellationToken) - => await RemoteJSDataStream.CreateRemoteJSDataStreamAsync(this, jsStreamReference, totalLength, maxBufferSize, _maximumIncomingBytes, _options.JSInteropDefaultCallTimeout, cancellationToken); + protected override async Task ReadJSDataAsStreamAsync(IJSStreamReference jsStreamReference, long totalLength, long pauseIncomingBytesThreshold = -1, long resumeIncomingBytesThreshold = -1, CancellationToken cancellationToken = default) + => await RemoteJSDataStream.CreateRemoteJSDataStreamAsync(this, jsStreamReference, totalLength, _maximumIncomingBytes, _options.JSInteropDefaultCallTimeout, pauseIncomingBytesThreshold, resumeIncomingBytesThreshold, cancellationToken); public static class Log { diff --git a/src/Components/Server/test/Circuits/RemoteJSDataStreamTest.cs b/src/Components/Server/test/Circuits/RemoteJSDataStreamTest.cs index 14dcf3eec46c..85cf6bd28957 100644 --- a/src/Components/Server/test/Circuits/RemoteJSDataStreamTest.cs +++ b/src/Components/Server/test/Circuits/RemoteJSDataStreamTest.cs @@ -4,8 +4,10 @@ using System; using System.IO; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.SignalR; +using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.JSInterop; @@ -19,34 +21,34 @@ public class RemoteJSDataStreamTest private static readonly TestRemoteJSRuntime _jsRuntime = new(Options.Create(new CircuitOptions()), Options.Create(new HubOptions()), Mock.Of>()); [Fact] - public async void CreateRemoteJSDataStreamAsync_CreatesStream() + public async Task CreateRemoteJSDataStreamAsync_CreatesStream() { // Arrange var jsStreamReference = Mock.Of(); // Act - var remoteJSDataStream = await RemoteJSDataStream.CreateRemoteJSDataStreamAsync(_jsRuntime, jsStreamReference, totalLength: 100, maxBufferSize: 50, maximumIncomingBytes: 10_000, jsInteropDefaultCallTimeout: TimeSpan.FromMinutes(1)); + var remoteJSDataStream = await RemoteJSDataStream.CreateRemoteJSDataStreamAsync(_jsRuntime, jsStreamReference, totalLength: 100, maximumIncomingBytes: 10_000, jsInteropDefaultCallTimeout: TimeSpan.FromMinutes(1), pauseIncomingBytesThreshold: 50, resumeIncomingBytesThreshold: 25, cancellationToken: CancellationToken.None).DefaultTimeout(); // Assert Assert.NotNull(remoteJSDataStream); } [Fact] - public async void ReceiveData_DoesNotFindStream() + public async Task ReceiveData_DoesNotFindStream() { // Arrange var chunk = new byte[] { 3, 5, 6, 7 }; var unrecognizedGuid = 10; // Act - var success = await RemoteJSDataStream.ReceiveData(_jsRuntime, streamId: unrecognizedGuid, chunkId: 0, chunk, error: null); + var success = await RemoteJSDataStream.ReceiveData(_jsRuntime, streamId: unrecognizedGuid, chunkId: 0, chunk, error: null).DefaultTimeout(); // Assert Assert.False(success); } [Fact] - public async void ReceiveData_SuccessReadsBackStream() + public async Task ReceiveData_SuccessReadsBackStream() { // Arrange var jsRuntime = new TestRemoteJSRuntime(Options.Create(new CircuitOptions()), Options.Create(new HubOptions()), Mock.Of>()); @@ -59,22 +61,50 @@ public async void ReceiveData_SuccessReadsBackStream() var sendDataTask = Task.Run(async () => { // Act 1 - var success = await RemoteJSDataStream.ReceiveData(jsRuntime, streamId, chunkId: 0, chunk, error: null); + var success = await RemoteJSDataStream.ReceiveData(jsRuntime, streamId, chunkId: 0, chunk, error: null).DefaultTimeout(); return success; }); // Act & Assert 2 using var memoryStream = new MemoryStream(); - await remoteJSDataStream.CopyToAsync(memoryStream); + await remoteJSDataStream.CopyToAsync(memoryStream).DefaultTimeout(); Assert.Equal(chunk, memoryStream.ToArray()); // Act & Assert 3 - var sendDataCompleted = await sendDataTask; + var sendDataCompleted = await sendDataTask.DefaultTimeout(); Assert.True(sendDataCompleted); } [Fact] - public async void ReceiveData_WithError() + public async Task ReceiveData_SuccessReadsBackPipeReader() + { + // Arrange + var jsRuntime = new TestRemoteJSRuntime(Options.Create(new CircuitOptions()), Options.Create(new HubOptions()), Mock.Of>()); + var remoteJSDataStream = await CreateRemoteJSDataStreamAsync(jsRuntime); + var streamId = GetStreamId(remoteJSDataStream, jsRuntime); + var chunk = new byte[100]; + var random = new Random(); + random.NextBytes(chunk); + + var sendDataTask = Task.Run(async () => + { + // Act 1 + var success = await RemoteJSDataStream.ReceiveData(jsRuntime, streamId, chunkId: 0, chunk, error: null).DefaultTimeout(); + return success; + }); + + // Act & Assert 2 + using var memoryStream = new MemoryStream(); + await remoteJSDataStream.PipeReader.CopyToAsync(memoryStream).DefaultTimeout(); + Assert.Equal(chunk, memoryStream.ToArray()); + + // Act & Assert 3 + var sendDataCompleted = await sendDataTask.DefaultTimeout(); + Assert.True(sendDataCompleted); + } + + [Fact] + public async Task ReceiveData_WithError() { // Arrange var jsRuntime = new TestRemoteJSRuntime(Options.Create(new CircuitOptions()), Options.Create(new HubOptions()), Mock.Of>()); @@ -82,17 +112,17 @@ public async void ReceiveData_WithError() var streamId = GetStreamId(remoteJSDataStream, jsRuntime); // Act & Assert 1 - var success = await RemoteJSDataStream.ReceiveData(jsRuntime, streamId, chunkId: 0, chunk: null, error: "some error"); + var success = await RemoteJSDataStream.ReceiveData(jsRuntime, streamId, chunkId: 0, chunk: null, error: "some error").DefaultTimeout(); Assert.False(success); // Act & Assert 2 using var mem = new MemoryStream(); - var ex = await Assert.ThrowsAsync(async () => await remoteJSDataStream.CopyToAsync(mem)); + var ex = await Assert.ThrowsAsync(async () => await remoteJSDataStream.CopyToAsync(mem).DefaultTimeout()); Assert.Equal("An error occurred while reading the remote stream: some error", ex.Message); } [Fact] - public async void ReceiveData_WithZeroLengthChunk() + public async Task ReceiveData_WithZeroLengthChunk() { // Arrange var jsRuntime = new TestRemoteJSRuntime(Options.Create(new CircuitOptions()), Options.Create(new HubOptions()), Mock.Of>()); @@ -101,42 +131,42 @@ public async void ReceiveData_WithZeroLengthChunk() var chunk = Array.Empty(); // Act & Assert 1 - var ex = await Assert.ThrowsAsync(async () => await RemoteJSDataStream.ReceiveData(jsRuntime, streamId, chunkId: 0, chunk, error: null)); + var ex = await Assert.ThrowsAsync(async () => await RemoteJSDataStream.ReceiveData(jsRuntime, streamId, chunkId: 0, chunk, error: null).DefaultTimeout()); Assert.Equal("The incoming data chunk cannot be empty.", ex.Message); // Act & Assert 2 using var mem = new MemoryStream(); - ex = await Assert.ThrowsAsync(async () => await remoteJSDataStream.CopyToAsync(mem)); + ex = await Assert.ThrowsAsync(async () => await remoteJSDataStream.CopyToAsync(mem).DefaultTimeout()); Assert.Equal("The incoming data chunk cannot be empty.", ex.Message); } [Fact] - public async void ReceiveData_ProvidedWithMoreBytesThanRemaining() + public async Task ReceiveData_ProvidedWithMoreBytesThanRemaining() { // Arrange var jsRuntime = new TestRemoteJSRuntime(Options.Create(new CircuitOptions()), Options.Create(new HubOptions()), Mock.Of>()); var jsStreamReference = Mock.Of(); - var remoteJSDataStream = await RemoteJSDataStream.CreateRemoteJSDataStreamAsync(jsRuntime, jsStreamReference, totalLength: 100, maxBufferSize: 50, maximumIncomingBytes: 10_000, jsInteropDefaultCallTimeout: TimeSpan.FromMinutes(1)); + var remoteJSDataStream = await RemoteJSDataStream.CreateRemoteJSDataStreamAsync(jsRuntime, jsStreamReference, totalLength: 100, maximumIncomingBytes: 10_000, jsInteropDefaultCallTimeout: TimeSpan.FromMinutes(1), pauseIncomingBytesThreshold: 50, resumeIncomingBytesThreshold: 25, cancellationToken: CancellationToken.None); var streamId = GetStreamId(remoteJSDataStream, jsRuntime); var chunk = new byte[110]; // 100 byte totalLength for stream // Act & Assert 1 - var ex = await Assert.ThrowsAsync(async () => await RemoteJSDataStream.ReceiveData(jsRuntime, streamId, chunkId: 0, chunk, error: null)); + var ex = await Assert.ThrowsAsync(async () => await RemoteJSDataStream.ReceiveData(jsRuntime, streamId, chunkId: 0, chunk, error: null).DefaultTimeout()); Assert.Equal("The incoming data stream declared a length 100, but 110 bytes were sent.", ex.Message); // Act & Assert 2 using var mem = new MemoryStream(); - ex = await Assert.ThrowsAsync(async () => await remoteJSDataStream.CopyToAsync(mem)); + ex = await Assert.ThrowsAsync(async () => await remoteJSDataStream.CopyToAsync(mem).DefaultTimeout()); Assert.Equal("The incoming data stream declared a length 100, but 110 bytes were sent.", ex.Message); } [Fact] - public async void ReceiveData_ProvidedWithOutOfOrderChunk_SimulatesSignalRDisconnect() + public async Task ReceiveData_ProvidedWithOutOfOrderChunk_SimulatesSignalRDisconnect() { // Arrange var jsRuntime = new TestRemoteJSRuntime(Options.Create(new CircuitOptions()), Options.Create(new HubOptions()), Mock.Of>()); var jsStreamReference = Mock.Of(); - var remoteJSDataStream = await RemoteJSDataStream.CreateRemoteJSDataStreamAsync(jsRuntime, jsStreamReference, totalLength: 100, maxBufferSize: 50, maximumIncomingBytes: 10_000, jsInteropDefaultCallTimeout: TimeSpan.FromMinutes(1)); + var remoteJSDataStream = await RemoteJSDataStream.CreateRemoteJSDataStreamAsync(jsRuntime, jsStreamReference, totalLength: 100, maximumIncomingBytes: 10_000, jsInteropDefaultCallTimeout: TimeSpan.FromMinutes(1), pauseIncomingBytesThreshold: 50, resumeIncomingBytesThreshold: 25, cancellationToken: CancellationToken.None); var streamId = GetStreamId(remoteJSDataStream, jsRuntime); var chunk = new byte[5]; @@ -145,19 +175,113 @@ public async void ReceiveData_ProvidedWithOutOfOrderChunk_SimulatesSignalRDiscon { await RemoteJSDataStream.ReceiveData(jsRuntime, streamId, chunkId: i, chunk, error: null); } - var ex = await Assert.ThrowsAsync(async () => await RemoteJSDataStream.ReceiveData(jsRuntime, streamId, chunkId: 7, chunk, error: null)); + var ex = await Assert.ThrowsAsync(async () => await RemoteJSDataStream.ReceiveData(jsRuntime, streamId, chunkId: 7, chunk, error: null).DefaultTimeout()); Assert.Equal("Out of sequence chunk received, expected 5, but received 7.", ex.Message); // Act & Assert 2 using var mem = new MemoryStream(); - ex = await Assert.ThrowsAsync(async () => await remoteJSDataStream.CopyToAsync(mem)); + ex = await Assert.ThrowsAsync(async () => await remoteJSDataStream.CopyToAsync(mem).DefaultTimeout()); Assert.Equal("Out of sequence chunk received, expected 5, but received 7.", ex.Message); } + [Fact] + public async Task ReceiveData_NoDataProvidedBeforeTimeout_StreamDisposed() + { + // Arrange + var unhandledExceptionRaisedTask = new TaskCompletionSource(); + var jsRuntime = new TestRemoteJSRuntime(Options.Create(new CircuitOptions()), Options.Create(new HubOptions()), Mock.Of>()); + jsRuntime.UnhandledException += (_, ex) => + { + Assert.Equal("Did not receive any data in the allotted time.", ex.Message); + unhandledExceptionRaisedTask.SetResult(ex is TimeoutException); + }; + + var jsStreamReference = Mock.Of(); + var remoteJSDataStream = await RemoteJSDataStream.CreateRemoteJSDataStreamAsync( + jsRuntime, + jsStreamReference, + totalLength: 15, + maximumIncomingBytes: 10_000, + jsInteropDefaultCallTimeout: TimeSpan.FromSeconds(2), + pauseIncomingBytesThreshold: 50, + resumeIncomingBytesThreshold: 25, + cancellationToken: CancellationToken.None); + var streamId = GetStreamId(remoteJSDataStream, jsRuntime); + var chunk = new byte[] { 3, 5, 7 }; + + // Act & Assert 1 + // Trigger timeout and ensure unhandled exception raised to crush circuit + remoteJSDataStream.InvalidateLastDataReceivedTimeForTimeout(); + var unhandledExceptionResult = await unhandledExceptionRaisedTask.Task.DefaultTimeout(); + Assert.True(unhandledExceptionResult); + + // Act & Assert 2 + // Confirm exception also raised on pipe reader + using var mem = new MemoryStream(); + var ex = await Assert.ThrowsAsync(async () => await remoteJSDataStream.CopyToAsync(mem).DefaultTimeout()); + Assert.Equal("Did not receive any data in the allotted time.", ex.Message); + + // Act & Assert 3 + // Ensures stream is disposed after the timeout and any additional chunks aren't accepted + var success = await RemoteJSDataStream.ReceiveData(jsRuntime, streamId, chunkId: 0, chunk, error: null).DefaultTimeout(); + Assert.False(success); + } + + [Fact] + public async Task ReceiveData_ReceivesDataThenTimesout_StreamDisposed() + { + // Arrange + var unhandledExceptionRaisedTask = new TaskCompletionSource(); + var jsRuntime = new TestRemoteJSRuntime(Options.Create(new CircuitOptions()), Options.Create(new HubOptions()), Mock.Of>()); + jsRuntime.UnhandledException += (_, ex) => + { + Assert.Equal("Did not receive any data in the allotted time.", ex.Message); + unhandledExceptionRaisedTask.SetResult(ex is TimeoutException); + }; + + var jsStreamReference = Mock.Of(); + var remoteJSDataStream = await RemoteJSDataStream.CreateRemoteJSDataStreamAsync( + jsRuntime, + jsStreamReference, + totalLength: 15, + maximumIncomingBytes: 10_000, + jsInteropDefaultCallTimeout: TimeSpan.FromSeconds(3), + pauseIncomingBytesThreshold: 50, + resumeIncomingBytesThreshold: 25, + cancellationToken: CancellationToken.None); + var streamId = GetStreamId(remoteJSDataStream, jsRuntime); + var chunk = new byte[] { 3, 5, 7 }; + + // Act & Assert 1 + var success = await RemoteJSDataStream.ReceiveData(jsRuntime, streamId, chunkId: 0, chunk, error: null).DefaultTimeout(); + Assert.True(success); + + // Act & Assert 2 + success = await RemoteJSDataStream.ReceiveData(jsRuntime, streamId, chunkId: 1, chunk, error: null).DefaultTimeout(); + Assert.True(success); + + // Act & Assert 3 + // Trigger timeout and ensure unhandled exception raised to crush circuit + remoteJSDataStream.InvalidateLastDataReceivedTimeForTimeout(); + var unhandledExceptionResult = await unhandledExceptionRaisedTask.Task.DefaultTimeout(); + Assert.True(unhandledExceptionResult); + + // Act & Assert 4 + // Confirm exception also raised on pipe reader + using var mem = new MemoryStream(); + var ex = await Assert.ThrowsAsync(async () => await remoteJSDataStream.CopyToAsync(mem).DefaultTimeout()); + Assert.Equal("Did not receive any data in the allotted time.", ex.Message); + + // Act & Assert 5 + // Ensures stream is disposed after the timeout and any additional chunks aren't accepted + success = await RemoteJSDataStream.ReceiveData(jsRuntime, streamId, chunkId: 2, chunk, error: null).DefaultTimeout(); + Assert.False(success); + } + private static async Task CreateRemoteJSDataStreamAsync(TestRemoteJSRuntime jsRuntime = null) { var jsStreamReference = Mock.Of(); - var remoteJSDataStream = await RemoteJSDataStream.CreateRemoteJSDataStreamAsync(jsRuntime ?? _jsRuntime, jsStreamReference, totalLength: 100, maxBufferSize: 50, maximumIncomingBytes: 10_000, jsInteropDefaultCallTimeout: TimeSpan.FromMinutes(1)); + var remoteJSDataStream = await RemoteJSDataStream.CreateRemoteJSDataStreamAsync(jsRuntime ?? _jsRuntime, jsStreamReference, totalLength: 100, maximumIncomingBytes: 10_000, jsInteropDefaultCallTimeout: TimeSpan.FromMinutes(1), pauseIncomingBytesThreshold: 50, resumeIncomingBytesThreshold: 25, cancellationToken: CancellationToken.None); return remoteJSDataStream; } diff --git a/src/Components/Web/src/Forms/InputFile/RemoteBrowserFileStream.cs b/src/Components/Web/src/Forms/InputFile/RemoteBrowserFileStream.cs index ddbee6e770db..9d633304a183 100644 --- a/src/Components/Web/src/Forms/InputFile/RemoteBrowserFileStream.cs +++ b/src/Components/Web/src/Forms/InputFile/RemoteBrowserFileStream.cs @@ -45,7 +45,11 @@ private async Task OpenReadStreamAsync(RemoteBrowserFileStreamOptions op _inputFileElement, File.Id); - return await dataReference.OpenReadStreamAsync(_maxAllowedSize, options.MaxBufferSize, cancellationToken); + return await dataReference.OpenReadStreamAsync( + _maxAllowedSize, + pauseIncomingBytesThreshold: options.MaxBufferSize, + resumeIncomingBytesThreshold: options.MaxBufferSize / 2, + cancellationToken); } protected override async ValueTask CopyFileDataIntoBuffer(long sourceOffset, Memory destination, CancellationToken cancellationToken) diff --git a/src/JSInterop/Microsoft.JSInterop/src/IJSStreamReference.cs b/src/JSInterop/Microsoft.JSInterop/src/IJSStreamReference.cs index 4cf01b5763f5..465bb6ddb08c 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/IJSStreamReference.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/IJSStreamReference.cs @@ -16,15 +16,25 @@ public interface IJSStreamReference : IAsyncDisposable /// /// Length of the provided by JavaScript. /// - public long Length { get; } + long Length { get; } /// /// Opens a with the for the current data reference. /// /// Maximum number of bytes permitted to be read from JavaScript. - /// Maximum number of bytes that are allowed to be buffered. + /// + /// The number of unconsumed bytes to accept from JS before blocking. + /// Defaults to -1, which indicates use of the default . + /// Avoid specifying an excessively large value because this could allow clients to exhaust memory. + /// A value of zero prevents JS from blocking, allowing .NET to receive an unlimited number of bytes. + /// + /// + /// The number of unflushed bytes at which point JS stops blocking. + /// Defaults to -1, which indicates use of the default . + /// Must be less than the to prevent thrashing at the limit. + /// /// for cancelling read. /// which can provide data associated with the current data reference. - ValueTask OpenReadStreamAsync(long maxAllowedSize = 512000, long maxBufferSize = 100 * 1024, CancellationToken cancellationToken = default); + ValueTask OpenReadStreamAsync(long maxAllowedSize = 512000, long pauseIncomingBytesThreshold = -1, long resumeIncomingBytesThreshold = -1, CancellationToken cancellationToken = default); } } diff --git a/src/JSInterop/Microsoft.JSInterop/src/Implementation/JSStreamReference.cs b/src/JSInterop/Microsoft.JSInterop/src/Implementation/JSStreamReference.cs index 269bcc9f87d8..a291c93b924d 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/Implementation/JSStreamReference.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/Implementation/JSStreamReference.cs @@ -36,14 +36,14 @@ internal JSStreamReference(JSRuntime jsRuntime, long id, long totalLength) : bas } /// - async ValueTask IJSStreamReference.OpenReadStreamAsync(long maxLength, long maxBufferSize, CancellationToken cancellationToken) + async ValueTask IJSStreamReference.OpenReadStreamAsync(long maxLength, long pauseIncomingBytesThreshold, long resumeIncomingBytesThreshold, CancellationToken cancellationToken) { if (Length > maxLength) { throw new ArgumentOutOfRangeException(nameof(maxLength), $"The incoming data stream of length {Length} exceeds the maximum length {maxLength}."); } - return await _jsRuntime.ReadJSDataAsStreamAsync(this, Length, maxBufferSize, cancellationToken); + return await _jsRuntime.ReadJSDataAsStreamAsync(this, Length, pauseIncomingBytesThreshold, resumeIncomingBytesThreshold, cancellationToken); } } } diff --git a/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs b/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs index 389166ab9b40..840dd8d702e1 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs +++ b/src/JSInterop/Microsoft.JSInterop/src/JSRuntime.cs @@ -215,10 +215,20 @@ protected internal virtual void ReceiveByteArray(int id, byte[] data) /// /// to produce a data stream for. /// Expected length of the incoming data stream. - /// Amount of bytes to buffer before flushing. + /// + /// The number of unconsumed bytes to accept from JS before blocking. + /// Defaults to -1, which indicates use of the default . + /// Avoid specifying an excessively large value because this could allow clients to exhaust memory. + /// A value of zero prevents JS from blocking, allowing .NET to receive an unlimited number of bytes. + /// + /// + /// The number of unflushed bytes at which point JS stops blocking. + /// Defaults to -1, which indicates use of the default . + /// Must be less than the to prevent thrashing at the limit. + /// /// for cancelling read. /// for the data reference represented by . - protected internal virtual Task ReadJSDataAsStreamAsync(IJSStreamReference jsStreamReference, long totalLength, long maxBufferSize, CancellationToken cancellationToken) + protected internal virtual Task ReadJSDataAsStreamAsync(IJSStreamReference jsStreamReference, long totalLength, long pauseIncomingBytesThreshold = -1, long resumeIncomingBytesThreshold = -1, CancellationToken cancellationToken = default) { // The reason it's virtual and not abstract is just for back-compat diff --git a/src/JSInterop/Microsoft.JSInterop/src/PublicAPI.Unshipped.txt b/src/JSInterop/Microsoft.JSInterop/src/PublicAPI.Unshipped.txt index e1e22df85d77..5eaf7afec314 100644 --- a/src/JSInterop/Microsoft.JSInterop/src/PublicAPI.Unshipped.txt +++ b/src/JSInterop/Microsoft.JSInterop/src/PublicAPI.Unshipped.txt @@ -1,7 +1,7 @@ #nullable enable Microsoft.JSInterop.IJSStreamReference Microsoft.JSInterop.IJSStreamReference.Length.get -> long -Microsoft.JSInterop.IJSStreamReference.OpenReadStreamAsync(long maxAllowedSize = 512000, long maxBufferSize = 102400, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask +Microsoft.JSInterop.IJSStreamReference.OpenReadStreamAsync(long maxAllowedSize = 512000, long pauseIncomingBytesThreshold = -1, long resumeIncomingBytesThreshold = -1, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask Microsoft.JSInterop.Implementation.JSStreamReference Microsoft.JSInterop.Implementation.JSStreamReference.Length.get -> long Microsoft.JSInterop.Implementation.JSObjectReferenceJsonWorker @@ -36,7 +36,7 @@ static Microsoft.JSInterop.JSRuntimeExtensions.InvokeVoidAsync(this Microsoft.JS static Microsoft.JSInterop.JSRuntimeExtensions.InvokeVoidAsync(this Microsoft.JSInterop.IJSRuntime! jsRuntime, string! identifier, System.TimeSpan timeout, params object?[]? args) -> System.Threading.Tasks.ValueTask *REMOVED*static Microsoft.JSInterop.JSRuntimeExtensions.InvokeVoidAsync(this Microsoft.JSInterop.IJSRuntime! jsRuntime, string! identifier, params object![]! args) -> System.Threading.Tasks.ValueTask static Microsoft.JSInterop.JSRuntimeExtensions.InvokeVoidAsync(this Microsoft.JSInterop.IJSRuntime! jsRuntime, string! identifier, params object?[]? args) -> System.Threading.Tasks.ValueTask -virtual Microsoft.JSInterop.JSRuntime.ReadJSDataAsStreamAsync(Microsoft.JSInterop.IJSStreamReference! jsStreamReference, long totalLength, long maxBufferSize, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +virtual Microsoft.JSInterop.JSRuntime.ReadJSDataAsStreamAsync(Microsoft.JSInterop.IJSStreamReference! jsStreamReference, long totalLength, long pauseIncomingBytesThreshold = -1, long resumeIncomingBytesThreshold = -1, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! virtual Microsoft.JSInterop.JSRuntime.ReceiveByteArray(int id, byte[]! data) -> void virtual Microsoft.JSInterop.JSRuntime.SendByteArray(int id, byte[]! data) -> void Microsoft.JSInterop.JSDisconnectedException diff --git a/src/JSInterop/Microsoft.JSInterop/test/JSRuntimeTest.cs b/src/JSInterop/Microsoft.JSInterop/test/JSRuntimeTest.cs index f887055564ac..8e27c40fe5a1 100644 --- a/src/JSInterop/Microsoft.JSInterop/test/JSRuntimeTest.cs +++ b/src/JSInterop/Microsoft.JSInterop/test/JSRuntimeTest.cs @@ -404,7 +404,7 @@ public async void ReadJSDataAsStreamAsync_ThrowsNotSupportedException() var dataReference = new JSStreamReference(runtime, 10, 10); // Act - var exception = await Assert.ThrowsAsync(async () => await runtime.ReadJSDataAsStreamAsync(dataReference, 10, 10, CancellationToken.None)); + var exception = await Assert.ThrowsAsync(async () => await runtime.ReadJSDataAsStreamAsync(dataReference, 10, 10, 10, CancellationToken.None)); // Assert Assert.Equal("The current JavaScript runtime does not support reading data streams.", exception.Message);