diff --git a/src/Components/Shared/src/PullFromJSDataStream.cs b/src/Components/Shared/src/PullFromJSDataStream.cs index aca42ee0cff6..dba94cbf5249 100644 --- a/src/Components/Shared/src/PullFromJSDataStream.cs +++ b/src/Components/Shared/src/PullFromJSDataStream.cs @@ -14,8 +14,9 @@ internal sealed class PullFromJSDataStream : Stream private readonly IJSRuntime _runtime; private readonly IJSStreamReference _jsStreamReference; private readonly long _totalLength; - private readonly CancellationToken _streamCancellationToken; + private readonly CancellationTokenSource _streamCts; private long _offset; + private bool _isDisposed; public static PullFromJSDataStream CreateJSDataStream( IJSRuntime runtime, @@ -36,8 +37,9 @@ private PullFromJSDataStream( _runtime = runtime; _jsStreamReference = jsStreamReference; _totalLength = totalLength; - _streamCancellationToken = cancellationToken; + _streamCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); _offset = 0; + } public override bool CanRead => true; @@ -88,7 +90,7 @@ public override async ValueTask ReadAsync(Memory buffer, Cancellation private void ThrowIfCancellationRequested(CancellationToken cancellationToken) { if (cancellationToken.IsCancellationRequested || - _streamCancellationToken.IsCancellationRequested) + _streamCts.IsCancellationRequested) { throw new TaskCanceledException(); } @@ -104,10 +106,52 @@ private async ValueTask RequestDataFromJSAsync(int numBytesToRead) } _offset += bytesRead.Length; - if (_offset == _totalLength) + return bytesRead; + } + + protected override void Dispose(bool disposing) + { + if (_isDisposed) { - Dispose(true); + return; } - return bytesRead; + _streamCts?.Cancel(); + _streamCts?.Dispose(); + try + { + _ = _jsStreamReference?.DisposeAsync().Preserve(); + } + catch + { + } + + _isDisposed = true; + + base.Dispose(disposing); + } + public override async ValueTask DisposeAsync() + { + if (_isDisposed) + { + return; + } + + _streamCts?.Cancel(); + _streamCts?.Dispose(); + + try + { + if (_jsStreamReference is not null) + { + await _jsStreamReference.DisposeAsync(); + } + } + catch + { + } + + _isDisposed = true; + + await base.DisposeAsync(); } } diff --git a/src/Components/WebAssembly/WebAssembly/test/PullFromJSDataStreamTest.cs b/src/Components/WebAssembly/WebAssembly/test/PullFromJSDataStreamTest.cs index bfe0f0f7a55a..407fcdb840ec 100644 --- a/src/Components/WebAssembly/WebAssembly/test/PullFromJSDataStreamTest.cs +++ b/src/Components/WebAssembly/WebAssembly/test/PullFromJSDataStreamTest.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Diagnostics.CodeAnalysis; using Microsoft.AspNetCore.InternalTesting; using Microsoft.JSInterop; using Moq; @@ -101,6 +100,28 @@ public async Task ReceiveData_JSProvidesExcessData_Throws2() Assert.Equal("Failed to read the requested number of bytes from the stream.", ex.Message); } + [Fact] + public void Dispose_CallsDisposeAsyncOnJSStreamReference() + { + var jsStreamReferenceMock = new Mock(); + var stream = PullFromJSDataStream.CreateJSDataStream(_jsRuntime, jsStreamReferenceMock.Object, totalLength: 10, cancellationToken: CancellationToken.None); + + stream.Dispose(); + + jsStreamReferenceMock.Verify(x => x.DisposeAsync(), Times.Once); + } + + [Fact] + public async Task DisposeAsync_CallsDisposeAsyncOnJSStreamReference() + { + var jsStreamReferenceMock = new Mock(); + var stream = PullFromJSDataStream.CreateJSDataStream(_jsRuntime, jsStreamReferenceMock.Object, totalLength: 10, cancellationToken: CancellationToken.None); + + await stream.DisposeAsync(); + + jsStreamReferenceMock.Verify(x => x.DisposeAsync(), Times.Once); + } + private static PullFromJSDataStream CreateJSDataStream(byte[] data, IJSRuntime runtime = null) { runtime ??= new TestJSRuntime(data);