-
Notifications
You must be signed in to change notification settings - Fork 10.4k
.NET-to-JS stream proof of concept #32848
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,19 @@ | ||
@page "/" | ||
@inject IJSRuntime JS | ||
|
||
<h1>Hello, world!</h1> | ||
|
||
Welcome to your new app. | ||
|
||
<button @onclick="SendStream">Send stream</button> | ||
|
||
<button onclick="getFileData()">Get file data (as return value)</button> | ||
|
||
@code { | ||
async Task SendStream() | ||
{ | ||
using var data = new System.IO.MemoryStream(new byte[10 * 1024 * 1024]); | ||
using var streamRef = new DotNetStreamReference(data); | ||
await JS.InvokeVoidAsync("getTheStream", streamRef); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -24,5 +24,17 @@ | |
}); | ||
})() | ||
</script> | ||
<script> | ||
async function getTheStream(streamRef) { | ||
const data = await streamRef.arrayBuffer(); | ||
console.log(data.byteLength); | ||
} | ||
|
||
async function getFileData() { | ||
const streamRef = await DotNet.invokeMethodAsync('BlazorServerApp', 'GetFileData'); | ||
const data = await streamRef.arrayBuffer(); | ||
console.log(data.byteLength); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is an example of receiving the stream as a return value. |
||
</script> | ||
</body> | ||
</html> |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,7 +2,11 @@ | |
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | ||
|
||
using System; | ||
using System.Collections.Concurrent; | ||
using System.IO; | ||
using System.Text.Json; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using Microsoft.AspNetCore.SignalR; | ||
using Microsoft.Extensions.Logging; | ||
using Microsoft.Extensions.Options; | ||
|
@@ -16,6 +20,7 @@ internal class RemoteJSRuntime : JSRuntime | |
private readonly CircuitOptions _options; | ||
private readonly ILogger<RemoteJSRuntime> _logger; | ||
private CircuitClientProxy _clientProxy; | ||
private ConcurrentDictionary<long, (Stream Stream, bool LeaveOpen)> _pendingStreams = new(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider Pipes instead of Streams? You wouldn't need to manage buffers in SendDotNetStreamAsync There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The Were you suggesting we should use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Right, I don't know the details of what users call, this just looked like a mostly internal thing. |
||
|
||
public ElementReferenceContext ElementReferenceContext { get; } | ||
|
||
|
@@ -89,6 +94,48 @@ protected override void BeginInvokeJS(long asyncHandle, string identifier, strin | |
_clientProxy.SendAsync("JS.BeginInvokeJS", asyncHandle, identifier, argsJson, (int)resultType, targetInstanceId); | ||
} | ||
|
||
protected override void BeginTransmittingStream(long streamId, Stream stream, bool leaveOpen) | ||
{ | ||
_ = TransmitStreamAsync(streamId, stream, leaveOpen); | ||
} | ||
|
||
private async Task TransmitStreamAsync(long streamId, Stream stream, bool leaveOpen) | ||
{ | ||
if (!_pendingStreams.TryAdd(streamId, (stream, leaveOpen))) | ||
{ | ||
throw new ArgumentException($"The stream {streamId} is already pending."); | ||
} | ||
|
||
// SignalR only supports streaming being initiated from the JS side, so we have to ask it to | ||
// start the stream. We'll give it a maximum of 10 seconds to do so, after which we give up | ||
// and discard it. | ||
var cancellationToken = new CancellationTokenSource(TimeSpan.FromSeconds(10)).Token; | ||
cancellationToken.Register(() => | ||
{ | ||
// If by now the stream hasn't been claimed for sending, stop tracking it | ||
if (_pendingStreams.TryRemove(streamId, out var timedOutStream) && !timedOutStream.LeaveOpen) | ||
{ | ||
timedOutStream.Stream.Dispose(); | ||
} | ||
}); | ||
|
||
await _clientProxy.SendAsync("JS.BeginTransmitStream", streamId); | ||
} | ||
|
||
public bool TryClaimPendingStreamForSending(long streamId, out Stream stream, out bool leaveOpen) | ||
{ | ||
if (_pendingStreams.TryRemove(streamId, out var pendingStream)) | ||
{ | ||
(stream, leaveOpen) = pendingStream; | ||
return true; | ||
} | ||
else | ||
{ | ||
(stream, leaveOpen) = (default, default); | ||
return false; | ||
} | ||
} | ||
|
||
public static class Log | ||
{ | ||
private static readonly Action<ILogger, long, string, Exception> _beginInvokeJS = | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
// Copyright (c) .NET Foundation. All rights reserved. | ||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | ||
|
||
using System; | ||
using System.IO; | ||
|
||
namespace Microsoft.JSInterop | ||
{ | ||
/// <summary> | ||
/// | ||
/// </summary> | ||
public sealed class DotNetStreamReference : IDisposable | ||
{ | ||
/// <summary> | ||
/// | ||
/// </summary> | ||
/// <param name="stream"></param> | ||
/// <param name="leaveOpen"></param> | ||
public DotNetStreamReference(Stream stream, bool leaveOpen = false) | ||
{ | ||
Stream = stream ?? throw new ArgumentNullException(nameof(stream)); | ||
LeaveOpen = leaveOpen; | ||
} | ||
|
||
internal Stream Stream { get; } | ||
|
||
internal bool LeaveOpen { get; } | ||
|
||
/// <inheritdoc /> | ||
public void Dispose() | ||
{ | ||
if (!LeaveOpen) | ||
{ | ||
Stream.Dispose(); | ||
} | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is an example of receiving the stream as a parameter.