Skip to content

Commit eccb20d

Browse files
authored
Blazor WebAssembly & WebView JS to .NET Streaming Interop Support (#33986)
* Prototype Large File Upload Support Blazor * Blob.slice Implementation * Allow WASM to keep working * Update CircuitStreamingInterop.ts * PR Feedback * Cleanup * WASM * WebView * Webview Remove SendJSDataStream * Refactor Shared Streaming Interop Code TS * WebView DotNetObjectReference Based ReceiveData * BaseJSDataStream Refactor * E2E tests WASM * Unhandled Exception * ReceiveJSDataChunk using DotNet Object Reference for WASM/WebView * WASM Unmarshalled Interop Support * E2E tests * Update BaseJSDataStream.cs * PR Feedback * Update Web(View, Assembly) to match interop followup items * PR Feedback * Updated Release .js * Create Microsoft.JSInterop.WebAssembly.WarningSuppressions.xml * Updated WASM/WebView Pull Based Implementation * InputFile Updated Implementation * Cleanup * PR Feedback * Remove ReadRequest * PR Feedback II * Tests * PR Feedback * Address `JS` to `DotNet` byte[] Interop API Review Feedback (#34328) * Address `JS` to `DotNet` byte[] Interop API Review Feedback Fixes: #34327 * Update RemoteJSDataStreamTest.cs * Update JSRuntimeTest.cs * Final PR Feedback * Fix WebView for large Files
1 parent e6afd50 commit eccb20d

39 files changed

+493
-390
lines changed

src/Components/Server/src/Circuits/RemoteJSDataStream.cs

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -40,22 +40,20 @@ public static async ValueTask<RemoteJSDataStream> CreateRemoteJSDataStreamAsync(
4040
RemoteJSRuntime runtime,
4141
IJSStreamReference jsStreamReference,
4242
long totalLength,
43-
long maximumIncomingBytes,
43+
long signalRMaximumIncomingBytes,
4444
TimeSpan jsInteropDefaultCallTimeout,
45-
long pauseIncomingBytesThreshold = -1,
46-
long resumeIncomingBytesThreshold = -1,
4745
CancellationToken cancellationToken = default)
4846
{
4947
// Enforce minimum 1 kb, maximum 50 kb, SignalR message size.
5048
// We budget 512 bytes overhead for the transfer, thus leaving at least 512 bytes for data
5149
// transfer per chunk with a 1 kb message size.
5250
// Additionally, to maintain interactivity, we put an upper limit of 50 kb on the message size.
53-
var chunkSize = maximumIncomingBytes > 1024 ?
54-
Math.Min(maximumIncomingBytes, 50*1024) - 512 :
51+
var chunkSize = signalRMaximumIncomingBytes > 1024 ?
52+
Math.Min(signalRMaximumIncomingBytes, 50*1024) - 512 :
5553
throw new ArgumentException($"SignalR MaximumIncomingBytes must be at least 1 kb.");
5654

5755
var streamId = runtime.RemoteJSDataStreamNextInstanceId++;
58-
var remoteJSDataStream = new RemoteJSDataStream(runtime, streamId, totalLength, jsInteropDefaultCallTimeout, pauseIncomingBytesThreshold, resumeIncomingBytesThreshold, cancellationToken);
56+
var remoteJSDataStream = new RemoteJSDataStream(runtime, streamId, totalLength, jsInteropDefaultCallTimeout, cancellationToken);
5957
await runtime.InvokeVoidAsync("Blazor._internal.sendJSDataStream", jsStreamReference, streamId, chunkSize);
6058
return remoteJSDataStream;
6159
}
@@ -65,8 +63,6 @@ private RemoteJSDataStream(
6563
long streamId,
6664
long totalLength,
6765
TimeSpan jsInteropDefaultCallTimeout,
68-
long pauseIncomingBytesThreshold,
69-
long resumeIncomingBytesThreshold,
7066
CancellationToken cancellationToken)
7167
{
7268
_runtime = runtime;
@@ -80,7 +76,7 @@ private RemoteJSDataStream(
8076

8177
_runtime.RemoteJSDataStreamInstances.Add(_streamId, this);
8278

83-
_pipe = new Pipe(new PipeOptions(pauseWriterThreshold: pauseIncomingBytesThreshold, resumeWriterThreshold: resumeIncomingBytesThreshold));
79+
_pipe = new Pipe();
8480
_pipeReaderStream = _pipe.Reader.AsStream();
8581
PipeReader = _pipe.Reader;
8682
}
@@ -94,9 +90,6 @@ private async Task<bool> ReceiveData(long chunkId, byte[] chunk, string error)
9490
{
9591
try
9692
{
97-
_lastDataReceivedTime = DateTimeOffset.UtcNow;
98-
_ = ThrowOnTimeout();
99-
10093
if (!string.IsNullOrEmpty(error))
10194
{
10295
throw new InvalidOperationException($"An error occurred while reading the remote stream: {error}");
@@ -111,7 +104,7 @@ private async Task<bool> ReceiveData(long chunkId, byte[] chunk, string error)
111104

112105
if (chunk.Length == 0)
113106
{
114-
throw new EndOfStreamException($"The incoming data chunk cannot be empty.");
107+
throw new EndOfStreamException("The incoming data chunk cannot be empty.");
115108
}
116109

117110
_bytesRead += chunk.Length;
@@ -121,6 +114,10 @@ private async Task<bool> ReceiveData(long chunkId, byte[] chunk, string error)
121114
throw new EndOfStreamException($"The incoming data stream declared a length {_totalLength}, but {_bytesRead} bytes were sent.");
122115
}
123116

117+
// Start timeout _after_ performing validations on data.
118+
_lastDataReceivedTime = DateTimeOffset.UtcNow;
119+
_ = ThrowOnTimeout();
120+
124121
await _pipe.Writer.WriteAsync(chunk, _streamCancellationToken);
125122

126123
if (_bytesRead == _totalLength)

src/Components/Server/src/Circuits/RemoteJSRuntime.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,8 +157,8 @@ public void MarkPermanentlyDisconnected()
157157
_clientProxy = null;
158158
}
159159

160-
protected override async Task<Stream> ReadJSDataAsStreamAsync(IJSStreamReference jsStreamReference, long totalLength, long pauseIncomingBytesThreshold = -1, long resumeIncomingBytesThreshold = -1, CancellationToken cancellationToken = default)
161-
=> await RemoteJSDataStream.CreateRemoteJSDataStreamAsync(this, jsStreamReference, totalLength, _maximumIncomingBytes, _options.JSInteropDefaultCallTimeout, pauseIncomingBytesThreshold, resumeIncomingBytesThreshold, cancellationToken);
160+
protected override async Task<Stream> ReadJSDataAsStreamAsync(IJSStreamReference jsStreamReference, long totalLength, CancellationToken cancellationToken = default)
161+
=> await RemoteJSDataStream.CreateRemoteJSDataStreamAsync(this, jsStreamReference, totalLength, _maximumIncomingBytes, _options.JSInteropDefaultCallTimeout, cancellationToken);
162162

163163
public static class Log
164164
{

src/Components/Server/src/Microsoft.AspNetCore.Components.Server.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
<Reference Include="Microsoft.Extensions.Logging" />
2626
<!-- Required for S.T.J source generation -->
2727
<Reference Include="System.Text.Json" PrivateAssets="All" />
28-
28+
2929
<Compile Include="$(SharedSourceRoot)ValueStopwatch\*.cs" />
3030

3131
<Compile Include="$(SharedSourceRoot)LinkerFlags.cs" LinkBase="Shared" />

src/Components/Server/test/Circuits/RemoteJSDataStreamTest.cs

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ public async Task CreateRemoteJSDataStreamAsync_CreatesStream()
2727
var jsStreamReference = Mock.Of<IJSStreamReference>();
2828

2929
// Act
30-
var remoteJSDataStream = await RemoteJSDataStream.CreateRemoteJSDataStreamAsync(_jsRuntime, jsStreamReference, totalLength: 100, maximumIncomingBytes: 10_000, jsInteropDefaultCallTimeout: TimeSpan.FromMinutes(1), pauseIncomingBytesThreshold: 50, resumeIncomingBytesThreshold: 25, cancellationToken: CancellationToken.None).DefaultTimeout();
30+
var remoteJSDataStream = await RemoteJSDataStream.CreateRemoteJSDataStreamAsync(_jsRuntime, jsStreamReference, totalLength: 100, signalRMaximumIncomingBytes: 10_000, jsInteropDefaultCallTimeout: TimeSpan.FromMinutes(1), cancellationToken: CancellationToken.None).DefaultTimeout();
3131

3232
// Assert
3333
Assert.NotNull(remoteJSDataStream);
@@ -146,7 +146,7 @@ public async Task ReceiveData_ProvidedWithMoreBytesThanRemaining()
146146
// Arrange
147147
var jsRuntime = new TestRemoteJSRuntime(Options.Create(new CircuitOptions()), Options.Create(new HubOptions()), Mock.Of<ILogger<RemoteJSRuntime>>());
148148
var jsStreamReference = Mock.Of<IJSStreamReference>();
149-
var remoteJSDataStream = await RemoteJSDataStream.CreateRemoteJSDataStreamAsync(jsRuntime, jsStreamReference, totalLength: 100, maximumIncomingBytes: 10_000, jsInteropDefaultCallTimeout: TimeSpan.FromMinutes(1), pauseIncomingBytesThreshold: 50, resumeIncomingBytesThreshold: 25, cancellationToken: CancellationToken.None);
149+
var remoteJSDataStream = await RemoteJSDataStream.CreateRemoteJSDataStreamAsync(jsRuntime, jsStreamReference, totalLength: 100, signalRMaximumIncomingBytes: 10_000, jsInteropDefaultCallTimeout: TimeSpan.FromMinutes(1), cancellationToken: CancellationToken.None);
150150
var streamId = GetStreamId(remoteJSDataStream, jsRuntime);
151151
var chunk = new byte[110]; // 100 byte totalLength for stream
152152

@@ -166,7 +166,7 @@ public async Task ReceiveData_ProvidedWithOutOfOrderChunk_SimulatesSignalRDiscon
166166
// Arrange
167167
var jsRuntime = new TestRemoteJSRuntime(Options.Create(new CircuitOptions()), Options.Create(new HubOptions()), Mock.Of<ILogger<RemoteJSRuntime>>());
168168
var jsStreamReference = Mock.Of<IJSStreamReference>();
169-
var remoteJSDataStream = await RemoteJSDataStream.CreateRemoteJSDataStreamAsync(jsRuntime, jsStreamReference, totalLength: 100, maximumIncomingBytes: 10_000, jsInteropDefaultCallTimeout: TimeSpan.FromMinutes(1), pauseIncomingBytesThreshold: 50, resumeIncomingBytesThreshold: 25, cancellationToken: CancellationToken.None);
169+
var remoteJSDataStream = await RemoteJSDataStream.CreateRemoteJSDataStreamAsync(jsRuntime, jsStreamReference, totalLength: 100, signalRMaximumIncomingBytes: 10_000, jsInteropDefaultCallTimeout: TimeSpan.FromMinutes(1), cancellationToken: CancellationToken.None);
170170
var streamId = GetStreamId(remoteJSDataStream, jsRuntime);
171171
var chunk = new byte[5];
172172

@@ -201,10 +201,8 @@ public async Task ReceiveData_NoDataProvidedBeforeTimeout_StreamDisposed()
201201
jsRuntime,
202202
jsStreamReference,
203203
totalLength: 15,
204-
maximumIncomingBytes: 10_000,
204+
signalRMaximumIncomingBytes: 10_000,
205205
jsInteropDefaultCallTimeout: TimeSpan.FromSeconds(2),
206-
pauseIncomingBytesThreshold: 50,
207-
resumeIncomingBytesThreshold: 25,
208206
cancellationToken: CancellationToken.None);
209207
var streamId = GetStreamId(remoteJSDataStream, jsRuntime);
210208
var chunk = new byte[] { 3, 5, 7 };
@@ -244,10 +242,8 @@ public async Task ReceiveData_ReceivesDataThenTimesout_StreamDisposed()
244242
jsRuntime,
245243
jsStreamReference,
246244
totalLength: 15,
247-
maximumIncomingBytes: 10_000,
245+
signalRMaximumIncomingBytes: 10_000,
248246
jsInteropDefaultCallTimeout: TimeSpan.FromSeconds(3),
249-
pauseIncomingBytesThreshold: 50,
250-
resumeIncomingBytesThreshold: 25,
251247
cancellationToken: CancellationToken.None);
252248
var streamId = GetStreamId(remoteJSDataStream, jsRuntime);
253249
var chunk = new byte[] { 3, 5, 7 };
@@ -281,7 +277,7 @@ public async Task ReceiveData_ReceivesDataThenTimesout_StreamDisposed()
281277
private static async Task<RemoteJSDataStream> CreateRemoteJSDataStreamAsync(TestRemoteJSRuntime jsRuntime = null)
282278
{
283279
var jsStreamReference = Mock.Of<IJSStreamReference>();
284-
var remoteJSDataStream = await RemoteJSDataStream.CreateRemoteJSDataStreamAsync(jsRuntime ?? _jsRuntime, jsStreamReference, totalLength: 100, maximumIncomingBytes: 10_000, jsInteropDefaultCallTimeout: TimeSpan.FromMinutes(1), pauseIncomingBytesThreshold: 50, resumeIncomingBytesThreshold: 25, cancellationToken: CancellationToken.None);
280+
var remoteJSDataStream = await RemoteJSDataStream.CreateRemoteJSDataStreamAsync(jsRuntime ?? _jsRuntime, jsStreamReference, totalLength: 100, signalRMaximumIncomingBytes: 10_000, jsInteropDefaultCallTimeout: TimeSpan.FromMinutes(1), cancellationToken: CancellationToken.None);
285281
return remoteJSDataStream;
286282
}
287283

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Threading;
6+
using System.Threading.Tasks;
7+
using Microsoft.JSInterop;
8+
9+
namespace Microsoft.AspNetCore.Components
10+
{
11+
/// <Summary>
12+
/// A stream that pulls each chunk on demand using JavaScript interop. This implementation is used for
13+
/// WebAssembly and WebView applications.
14+
/// </Summary>
15+
internal sealed class PullFromJSDataStream : Stream
16+
{
17+
private readonly IJSRuntime _runtime;
18+
private readonly IJSStreamReference _jsStreamReference;
19+
private readonly long _totalLength;
20+
private readonly CancellationToken _streamCancellationToken;
21+
private long _offset;
22+
23+
public static PullFromJSDataStream CreateJSDataStream(
24+
IJSRuntime runtime,
25+
IJSStreamReference jsStreamReference,
26+
long totalLength,
27+
CancellationToken cancellationToken = default)
28+
{
29+
var jsDataStream = new PullFromJSDataStream(runtime, jsStreamReference, totalLength, cancellationToken);
30+
return jsDataStream;
31+
}
32+
33+
private PullFromJSDataStream(
34+
IJSRuntime runtime,
35+
IJSStreamReference jsStreamReference,
36+
long totalLength,
37+
CancellationToken cancellationToken)
38+
{
39+
_runtime = runtime;
40+
_jsStreamReference = jsStreamReference;
41+
_totalLength = totalLength;
42+
_streamCancellationToken = cancellationToken;
43+
_offset = 0;
44+
}
45+
46+
public override bool CanRead => true;
47+
48+
public override bool CanSeek => false;
49+
50+
public override bool CanWrite => false;
51+
52+
public override long Length => _totalLength;
53+
54+
public override long Position
55+
{
56+
get => _offset;
57+
set => throw new NotSupportedException();
58+
}
59+
60+
public override void Flush()
61+
=> throw new NotSupportedException();
62+
63+
public override int Read(byte[] buffer, int offset, int count)
64+
=> throw new NotSupportedException("Synchronous reads are not supported.");
65+
66+
public override long Seek(long offset, SeekOrigin origin)
67+
=> throw new NotSupportedException();
68+
69+
public override void SetLength(long value)
70+
=> throw new NotSupportedException();
71+
72+
public override void Write(byte[] buffer, int offset, int count)
73+
=> throw new NotSupportedException();
74+
75+
public override async Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
76+
=> await ReadAsync(buffer.AsMemory(offset, count), cancellationToken);
77+
78+
public override async ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
79+
{
80+
var bytesRead = await RequestDataFromJSAsync(buffer.Length);
81+
ThrowIfCancellationRequested(cancellationToken);
82+
bytesRead.CopyTo(buffer);
83+
84+
return bytesRead.Length;
85+
}
86+
87+
private void ThrowIfCancellationRequested(CancellationToken cancellationToken)
88+
{
89+
if (cancellationToken.IsCancellationRequested ||
90+
_streamCancellationToken.IsCancellationRequested)
91+
{
92+
throw new TaskCanceledException();
93+
}
94+
}
95+
96+
private async ValueTask<byte[]> RequestDataFromJSAsync(int numBytesToRead)
97+
{
98+
numBytesToRead = (int)Math.Min(numBytesToRead, _totalLength - _offset);
99+
var bytesRead = await _runtime.InvokeAsync<byte[]>("Blazor._internal.getJSDataStreamChunk", _jsStreamReference, _offset, numBytesToRead);
100+
if (bytesRead.Length != numBytesToRead)
101+
{
102+
throw new EndOfStreamException("Failed to read the requested number of bytes from the stream.");
103+
}
104+
105+
_offset += bytesRead.Length;
106+
if (_offset == _totalLength)
107+
{
108+
Dispose(true);
109+
}
110+
return bytesRead;
111+
}
112+
}
113+
}

src/Components/Web.JS/dist/Release/blazor.server.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Components/Web.JS/dist/Release/blazor.webview.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Components/Web.JS/src/Boot.Server.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import { resolveOptions, CircuitStartOptions } from './Platform/Circuits/Circuit
1313
import { DefaultReconnectionHandler } from './Platform/Circuits/DefaultReconnectionHandler';
1414
import { attachRootComponentToLogicalElement } from './Rendering/Renderer';
1515
import { discoverComponents, discoverPersistedState, ServerComponentDescriptor } from './Services/ComponentDescriptorDiscovery';
16-
import { InputFile } from './InputFile';
1716
import { sendJSDataStream } from './Platform/Circuits/CircuitStreamingInterop';
1817

1918
let renderingFailed = false;
@@ -29,7 +28,6 @@ async function boot(userOptions?: Partial<CircuitStartOptions>): Promise<void> {
2928
const options = resolveOptions(userOptions);
3029
const logger = new ConsoleLogger(options.logLevel);
3130
Blazor.defaultReconnectionHandler = new DefaultReconnectionHandler(logger);
32-
Blazor._internal.InputFile = InputFile;
3331

3432
options.reconnectionHandler = options.reconnectionHandler || Blazor.defaultReconnectionHandler;
3533
logger.log(LogLevel.Information, 'Starting up Blazor server-side application.');
@@ -126,7 +124,7 @@ async function initializeConnection(options: CircuitStartOptions, logger: Logger
126124

127125
Blazor._internal.forceCloseConnection = () => connection.stop();
128126

129-
Blazor._internal.sendJSDataStream = (data: ArrayBufferView, streamId: string, chunkSize: number) => sendJSDataStream(connection, data, streamId, chunkSize);
127+
Blazor._internal.sendJSDataStream = (data: ArrayBufferView | Blob, streamId: number, chunkSize: number) => sendJSDataStream(connection, data, streamId, chunkSize);
130128

131129
try {
132130
await connection.start();

src/Components/Web.JS/src/Boot.WebAssembly.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import { Pointer, System_Array, System_Boolean, System_Byte, System_Int, System_
1414
import { WebAssemblyStartOptions } from './Platform/WebAssemblyStartOptions';
1515
import { WebAssemblyComponentAttacher } from './Platform/WebAssemblyComponentAttacher';
1616
import { discoverComponents, discoverPersistedState, WebAssemblyComponentDescriptor } from './Services/ComponentDescriptorDiscovery';
17-
import { WasmInputFile } from './WasmInputFile';
1817

1918
declare var Module: EmscriptenModule;
2019
let started = false;
@@ -43,8 +42,6 @@ async function boot(options?: Partial<WebAssemblyStartOptions>): Promise<void> {
4342
}
4443
});
4544

46-
Blazor._internal.InputFile = WasmInputFile;
47-
4845
Blazor._internal.applyHotReload = (id: string, metadataDelta: string, ilDeta: string) => {
4946
DotNet.invokeMethod('Microsoft.AspNetCore.Components.WebAssembly', 'ApplyHotReloadDelta', id, metadataDelta, ilDeta);
5047
};
@@ -160,6 +157,9 @@ function invokeJSFromDotNet(callInfo: Pointer, arg0: any, arg1: any, arg2: any):
160157
case DotNet.JSCallResultType.JSObjectReference:
161158
return DotNet.createJSObjectReference(result).__jsObjectId;
162159
case DotNet.JSCallResultType.JSStreamReference:
160+
const streamReference = DotNet.createJSStreamReference(result);
161+
const resultJson = JSON.stringify(streamReference);
162+
return BINDING.js_string_to_mono_string(resultJson);
163163
default:
164164
throw new Error(`Invalid JS call result type '${resultType}'.`);
165165
}

src/Components/Web.JS/src/Boot.WebView.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { internalFunctions as navigationManagerFunctions } from './Services/Navi
55
import { setEventDispatcher } from './Rendering/Events/EventDispatcher';
66
import { startIpcReceiver } from './Platform/WebView/WebViewIpcReceiver';
77
import { sendBrowserEvent, sendAttachPage, sendBeginInvokeDotNetFromJS, sendEndInvokeJSFromDotNet, sendByteArray, sendLocationChanged } from './Platform/WebView/WebViewIpcSender';
8-
import { InputFile } from './InputFile';
98

109
let started = false;
1110

@@ -23,7 +22,6 @@ async function boot(): Promise<void> {
2322
sendByteArray: sendByteArray,
2423
});
2524

26-
Blazor._internal.InputFile = InputFile;
2725
navigationManagerFunctions.enableNavigationInterception();
2826
navigationManagerFunctions.listenForNavigationEvents(sendLocationChanged);
2927

src/Components/Web.JS/src/GlobalExports.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { DefaultReconnectionHandler } from './Platform/Circuits/DefaultReconnect
88
import { CircuitStartOptions } from './Platform/Circuits/CircuitStartOptions';
99
import { WebAssemblyStartOptions } from './Platform/WebAssemblyStartOptions';
1010
import { Platform, Pointer, System_String, System_Array, System_Object, System_Boolean, System_Byte, System_Int } from './Platform/Platform';
11+
import { getNextChunk } from './StreamingInterop';
1112

1213
interface IBlazor {
1314
navigateTo: (uri: string, options: NavigationOptions) => void;
@@ -48,7 +49,8 @@ interface IBlazor {
4849
getLazyAssemblies?: any
4950
dotNetCriticalError?: any
5051
getSatelliteAssemblies?: any,
51-
sendJSDataStream?: (data: any, streamId: string, chunkSize: number) => void,
52+
sendJSDataStream?: (data: any, streamId: number, chunkSize: number) => void,
53+
getJSDataStreamChunk?: (data: any, position: number, chunkSize: number) => Promise<Uint8Array>,
5254

5355
// APIs invoked by hot reload
5456
applyHotReload?: (id: string, metadataDelta: string, ilDelta: string) => void,
@@ -64,6 +66,8 @@ export const Blazor: IBlazor = {
6466
navigationManager: navigationManagerInternalFunctions,
6567
domWrapper: domFunctions,
6668
Virtualize,
69+
InputFile,
70+
getJSDataStreamChunk: getNextChunk,
6771
},
6872
};
6973

0 commit comments

Comments
 (0)