-
Notifications
You must be signed in to change notification settings - Fork 10.5k
.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.
| 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
ReadableStreamwhich you can then easily convert asynchronously into anArrayBufferor you can convert into aBlobto be used as a response (e.g., for image contents).The sequence of operations is:
DotNetStreamReferenceto wrap someStreamJSRuntimebase class instructs the hosting platform to supply theStreamto 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.DotNetStreamReferenceas 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:
ReadableStreaminstance 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.Streamcontents 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
ArrayBufferinstead 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.