Open NickDarvey opened 1 year ago
Hi @NickDarvey,
Thanks for your advice! To help better understand your need, could you describe the specific usage scenario in detail?
@plantree, I'm building a hybrid app where I have some web components (JS) hosted in a native (WinUI3) application.
I want my web components to be able to use services offered by my application so that, for example:
the web component can react to application lifecycle events raised in the native app
the web component can display large text documents from a zip file that was uncompressed in the native app
Both examples would benefit from being able to intercept a HTTP request and stream responses back to the client.
For (1), the web component could subscribe to the lifecycle events with a fetch to http://0.0.0.0/app/lifecycle
and the native app could publish each lifecycle event using server-sent events (or some binary type-length-value encoding), so the web component can process them as a sequence.
For (2), the web component could request the document with a fetch to http://0.0.0.0/packages/1234/docs/abcd
and the native app could return unzip and return the document line-by-line, so the web component can display it progressively.
The alternative is to correlate successive web messages (CoreWebView2.PostWebMessageAsJson
) back into a sequence with the downside of (a) needing to do the correlation and (b) needing to marshal everything to a string.
This functionality is available on other platforms. For example, on iOS and macOS with WKWebView:
After you load some portion of the resource data, call the didReceiveData: method to send it to WebKit. You may call that method multiple times to deliver data incrementally, or call it once with all of the data.
https://developer.apple.com/documentation/webkit/wkurlschemetask
Hi @NickDarvey,
Thanks for your information and professional analysis!
If it's possible, could you provide a simple .sln
project to help better service this feature. Thanks.
@plantree, I think the code I provided in my original post is a simple example of how it might work.
Is this item saying sse is not currently supported by webview2?
Is this item saying sse is not currently supported by webview2?
@PylotLight, SSE is not supported if you’re intercepting requests with WebView2’s WebResourceRequested filters, but it works fine otherwise.
Hi Nick - I will track this feature request in our internal team backlog. In the meantime can you help me understand if you currently have a workaround ( my assumption is no? )
I wrapped fetch
body
's getReader
to communicate with the app.
Javascript part:
(function () {
'use strict';
(function () {
if (!window.onKAppEventStream) {
const eventRegisters = {};
const onKAppEventStream = (data) => {
let { uuid, final, content } = data;
if (!eventRegisters[uuid]) {
eventRegisters[uuid] = {
instance: null,
pending: [],
};
}
if (final) {
content = null;
} else {
if (typeof content === 'string') {
content = new TextEncoder().encode(content);
}
}
eventRegisters[uuid].pending.push({ final, content });
if (eventRegisters[uuid].instance) {
eventRegisters[uuid].instance.notifyMessage();
}
};
Object.defineProperty(window, 'onKAppEventStream', {
value: onKAppEventStream,
configurable: true,
writable: true,
enumerable: false,
});
class KAppFetchEventStream {
constructor(eventStreamId) {
this.eventStreamId = eventStreamId;
this.finished = false;
this.finishResolver = new Promise(resolve => this.runFinishResolve = resolve);
}
cancel() {
return this.finishResolver;
}
notifyMessage() {
if (this.messageNotifier) {
const noti = this.messageNotifier;
this.messageNotifier = null;
noti();
}
}
onFinish() {
if (!this.finished) {
this.finished = true;
this.runFinishResolve();
delete eventRegisters[this.eventStreamId];
}
}
triggerFinal() {
if (!this.finished) {
setTimeout(() => this.onFinish(), 100);
}
}
async read() {
if (!eventRegisters[this.eventStreamId]) {
eventRegisters[this.eventStreamId] = {
instance: this,
pending: [],
};
} else {
eventRegisters[this.eventStreamId].instance = this;
}
if (eventRegisters[this.eventStreamId].pending.length == 0) {
while (this.messageNotifier) {
await new Promise(resolve => setTimeout(resolve, 100));
}
await new Promise(resolve => this.messageNotifier = resolve);
}
if (eventRegisters[this.eventStreamId].pending.length > 0) {
const { final, content } = eventRegisters[this.eventStreamId].pending.shift();
if (final) this.triggerFinal();
return { done: final, value: content };
}
}
releaseLock() {}
}
((fetch) => {
window.fetch = async function (uri, options, ...args) {
let r = await fetch.call(this, uri, options, ...args);
let eventStreamId = r.headers.get('kapp-event-stream');
if (eventStreamId) {
r.body.getReader = () => {
return new KAppFetchEventStream(eventStreamId);
}
}
return r;
};
})(fetch);
}
})();
})();
C# part:
...
// in handleWebResourceRequested:, when we get an event-stream response, just finish the response and use simulateStreamResponse to send parts to the website
if (contentType != null && contentType.Split(';')[0] == "text/event-stream")
{
var uuid = Guid.NewGuid().ToString();
_ = simulateStreamResponse(uuid, responseStream);
var emptyResponseStream = new InMemoryRandomAccessStream();
return contentWebView.CoreWebView2.Environment.CreateWebResourceResponse(
emptyResponseStream.AsStreamForRead(),
(int)response.StatusCode,
response.ReasonPhrase,
build_reponse_header(response, new_length: 0, additional: new string[] { $"kapp-event-stream: {uuid}" })
);
}
...
void sendStreamResponse(string uuid, bool final, string? content)
{
BeginInvoke(async () =>
{
var finalJS = final ? "true" : "false";
if (content == null)
{
await contentWebView.ExecuteScriptAsync($"(window.onKAppEventStream||console.log)({{uuid:'{uuid}',final:{finalJS},content:null}});");
}
else
{
var bytes = System.Text.Encoding.UTF8.GetBytes(content);
var bytesString = "[" + String.Join(",", bytes) + "]";
var eval = $"(window.onKAppEventStream||console.log)({{uuid:'{uuid}',final:{finalJS},content:Uint8Array.from({bytesString})}});";
await contentWebView.ExecuteScriptAsync(eval);
}
});
}
async Task simulateStreamResponse(string uuid, Stream? stream)
{
try
{
if (stream == null)
{
return;
}
var reader = new StreamReader(stream);
var line = "";
while (!reader.EndOfStream && (line = await reader.ReadLineAsync()) != null)
{
sendStreamResponse(uuid, false, line + "\n");
await Task.Delay(10);
}
}
finally
{
sendStreamResponse(uuid, true, null);
}
}
I tried to implement Response Streaming for Wails and for that purpose did a custom implementation of IStream
on the Go side and gave it back as ICoreWebView2WebResourceResponse.Content
. The problem is, that it seems like WebView2 only uses one background thread for reading all the response Streams. So If one of the IStream's Read
method is blocking, the whole request processing also for other requests is blocked as well. For SSE it is not possible to have the whole content available when finishing the deferral as documented here.
Stream must have all the content data available by the time the WebResourceRequested event deferral of this response is completed. Stream should be agile or be created from a background thread to prevent performance impact to the UI thread.
So this would need something like ReadAsync support from IStreamAsync.
Furthermore we would also need a way to find out if the request has been stopped by WebView2. For example let's say one does an SSE streaming and WebView2 does a reload of the document. In that case we would need a way to get informed that the request is getting stopped and the stop the SSE streaming process on the host side.
@victorhuangwq @yildirimcagri-msft Any suggestions based on the comment from staffabi above?
Any updates? We are in a similar situation, we want to stream HTTP responses to the client, tipically for download large files. It seems the response becomes available all-at-once to the client js.
@vbryh-msft could you help us understand if this is something can already be done? And if this feature request is valid?
skimmed through request and comments - does SharedBuffer API can be used there?
SharedBuffer API does not help in this case, we would like to stream HTTP responses back. So the frontend code could use a simple fetch call or the EventSource API.
+1 for this use case. I am looking at using something like this mostly for improved performance in getting data from the backend to the frontend, and while the SharedBuffer API could also provide this performance increase, it is a much lower-level API and requires lots of manual synchronization implementation which makes it an order of magnitude more complex.
Any news on this team? This is quite a high priority for us as both Mac and Linux equivalents have it and the support on Windows is lagging. Appreciate your work! 🙏
I want to stream HTTP responses to WebView2. I don't think this is possible right now, so this is a feature request.
For example, I want to intercept a request and return server-sent events which the client can process per event.
For example, I want to intercept a request and return a chunked response which the client can process incrementally.
Repro
(Or how it could work)
Host C# code
Client JavaScript code
Expected
I want the response to be streamed so the client can process each event as it's sent.
For example, running the JavaScript client code with the url
https://sse.dev/test
will behave like this.Actual
The response is buffered and becomes available to the client all-at-once.
System info
AB#47606624