-
Notifications
You must be signed in to change notification settings - Fork 10.3k
.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
Conversation
async function getTheStream(streamRef) { | ||
const data = await streamRef.arrayBuffer(); | ||
console.log(data.byteLength); | ||
} |
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.
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 comment
The reason will be displayed to describe this comment to others. Learn more.
This is an example of receiving the stream as a return value.
@@ -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 comment
The 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 comment
The reason will be displayed to describe this comment to others. Learn more.
The Stream
instance here comes from user code, which is a more familiar and flexible API to offer.
Were you suggesting we should use Pipe
as the API in user code (as in, they must supply a Pipe
to us), or were you suggesting this for only if it was an internal detail?
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.
were you suggesting this for only if it was an internal detail?
Right, I don't know the details of what users call, this just looked like a mostly internal thing.
This proof-of-concept is no longer required as it was superseded by a real implementation. |
This is just an exploration, not a real PR. I think it may be @TanayParikh doing the real implementation, but I'm not sure if that's confirmed yet.
It is a bit complicated, but hopefully achieves what we need in terms of not pinning things in memory indefinitely on the .NET side. It's also very flexible on the JS side, as you can receive a
ReadableStream
which you can then easily convert asynchronously into anArrayBuffer
or you can convert into aBlob
to be used as a response (e.g., for image contents).The sequence of operations is:
DotNetStreamReference
to wrap someStream
JSRuntime
base class instructs the hosting platform to supply theStream
to JS code under the same ID, by calling a protected virtual methodBeginTransmittingStream
.DotNet.jsCallDispatcher.supplyDotNetStream(streamId, readableStream)
. The details of the transport, chunking, multiplexing, etc. are all left up to the hosting platform.DotNetStreamReference
as an instance of a JS class that has a functionstream()
that returns aPromise<ReadableStream>
, whose value is given by what is supplied in step 3 aboveAs you see, there's nothing on the .NET side that has to hold onto anything.
The way that step 3 works depends on the hosting platform. The most complex one is Blazor Server, which is what I've prototyped in this PR. Since SignalR only allows streaming to be initiated from the JS side, what it does is:
ReadableStream
instance and wires up the SignalR stream response to provide content for it. This becomes the stream instance returned to user code from step 4 above.Stream
contents into the SignalR stream response as fast as we're allowed (which can take arbitrarily long), then finally closes it.Open questions
Alternative considered
If we wanted, we could change the flow so that the .NET side pumps the entire stream contents across before the JS-side invocation occurs. Then the JS-side code could receive an
ArrayBuffer
instead of a stream.TBH I'm not convinced that actually solves any of the tricky things here, and it loses the ability to actually read the stream data incrementally in JS.
Or even more aggressively, we could just pump out the entire stream as a
byte[]
in the initial SignalR message. This might simplify a lot of the moving parts, but would massively reduce the usefulness of this because (I think) it would no longer multiplex at all and would block the entire circuit from being interactive until the whole transfer completes.