Skip to content

Commit d9b636c

Browse files
Enable Blazor Server and WebAssembly components to run in the same document (#48294)
1 parent f6e63eb commit d9b636c

32 files changed

+872
-753
lines changed

src/Components/Components.slnf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,4 +148,4 @@
148148
"src\\WebEncoders\\src\\Microsoft.Extensions.WebEncoders.csproj"
149149
]
150150
}
151-
}
151+
}

src/Components/Shared/src/TransmitDataStreamToJS.cs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ namespace Microsoft.AspNetCore.Components;
1212
/// </Summary>
1313
internal static class TransmitDataStreamToJS
1414
{
15-
internal static async Task TransmitStreamAsync(IJSRuntime runtime, long streamId, DotNetStreamReference dotNetStreamReference)
15+
internal static async Task TransmitStreamAsync(IJSRuntime runtime, string methodIdentifier, long streamId, DotNetStreamReference dotNetStreamReference)
1616
{
1717
var buffer = ArrayPool<byte>.Shared.Rent(32 * 1024);
1818

@@ -21,18 +21,18 @@ internal static async Task TransmitStreamAsync(IJSRuntime runtime, long streamId
2121
int bytesRead;
2222
while ((bytesRead = await dotNetStreamReference.Stream.ReadAsync(buffer)) > 0)
2323
{
24-
await runtime.InvokeVoidAsync("Blazor._internal.receiveDotNetDataStream", streamId, buffer, bytesRead, null);
24+
await runtime.InvokeVoidAsync(methodIdentifier, streamId, buffer, bytesRead, null);
2525
}
2626

2727
// Notify client that the stream has completed
28-
await runtime.InvokeVoidAsync("Blazor._internal.receiveDotNetDataStream", streamId, Array.Empty<byte>(), 0, null);
28+
await runtime.InvokeVoidAsync(methodIdentifier, streamId, Array.Empty<byte>(), 0, null);
2929
}
3030
catch (Exception ex)
3131
{
3232
try
3333
{
3434
// Attempt to notify the client of the error.
35-
await runtime.InvokeVoidAsync("Blazor._internal.receiveDotNetDataStream", streamId, Array.Empty<byte>(), 0, ex.Message);
35+
await runtime.InvokeVoidAsync(methodIdentifier, streamId, Array.Empty<byte>(), 0, ex.Message);
3636
}
3737
catch
3838
{
@@ -51,5 +51,4 @@ internal static async Task TransmitStreamAsync(IJSRuntime runtime, long streamId
5151
}
5252
}
5353
}
54-
5554
}

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.web.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.
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
import { DotNet } from '@microsoft/dotnet-js-interop';
5+
import { Blazor } from './GlobalExports';
6+
import { HubConnectionBuilder, HubConnection, HttpTransportType } from '@microsoft/signalr';
7+
import { MessagePackHubProtocol } from '@microsoft/signalr-protocol-msgpack';
8+
import { showErrorNotification } from './BootErrors';
9+
import { RenderQueue } from './Platform/Circuits/RenderQueue';
10+
import { ConsoleLogger } from './Platform/Logging/Loggers';
11+
import { LogLevel, Logger } from './Platform/Logging/Logger';
12+
import { CircuitDescriptor } from './Platform/Circuits/CircuitManager';
13+
import { resolveOptions, CircuitStartOptions } from './Platform/Circuits/CircuitStartOptions';
14+
import { DefaultReconnectionHandler } from './Platform/Circuits/DefaultReconnectionHandler';
15+
import { attachRootComponentToLogicalElement } from './Rendering/Renderer';
16+
import { discoverPersistedState, ServerComponentDescriptor } from './Services/ComponentDescriptorDiscovery';
17+
import { sendJSDataStream } from './Platform/Circuits/CircuitStreamingInterop';
18+
import { fetchAndInvokeInitializers } from './JSInitializers/JSInitializers.Server';
19+
20+
let renderingFailed = false;
21+
let connection: HubConnection;
22+
let circuit: CircuitDescriptor;
23+
let dispatcher: DotNet.ICallDispatcher;
24+
25+
export async function startCircuit(userOptions?: Partial<CircuitStartOptions>, components?: ServerComponentDescriptor[]): Promise<void> {
26+
// Establish options to be used
27+
const options = resolveOptions(userOptions);
28+
const jsInitializer = await fetchAndInvokeInitializers(options);
29+
30+
const logger = new ConsoleLogger(options.logLevel);
31+
32+
Blazor.reconnect = async (existingConnection?: HubConnection): Promise<boolean> => {
33+
if (renderingFailed) {
34+
// We can't reconnect after a failure, so exit early.
35+
return false;
36+
}
37+
38+
const reconnection = existingConnection || await initializeConnection(options, logger, circuit);
39+
if (!(await circuit.reconnect(reconnection))) {
40+
logger.log(LogLevel.Information, 'Reconnection attempt to the circuit was rejected by the server. This may indicate that the associated state is no longer available on the server.');
41+
return false;
42+
}
43+
44+
options.reconnectionHandler!.onConnectionUp();
45+
46+
return true;
47+
};
48+
Blazor.defaultReconnectionHandler = new DefaultReconnectionHandler(logger);
49+
50+
options.reconnectionHandler = options.reconnectionHandler || Blazor.defaultReconnectionHandler;
51+
logger.log(LogLevel.Information, 'Starting up Blazor server-side application.');
52+
53+
const appState = discoverPersistedState(document);
54+
circuit = new CircuitDescriptor(components || [], appState || '');
55+
56+
// Configure navigation via SignalR
57+
Blazor._internal.navigationManager.listenForNavigationEvents((uri: string, state: string | undefined, intercepted: boolean): Promise<void> => {
58+
return connection.send('OnLocationChanged', uri, state, intercepted);
59+
}, (callId: number, uri: string, state: string | undefined, intercepted: boolean): Promise<void> => {
60+
return connection.send('OnLocationChanging', callId, uri, state, intercepted);
61+
});
62+
63+
Blazor._internal.forceCloseConnection = () => connection.stop();
64+
Blazor._internal.sendJSDataStream = (data: ArrayBufferView | Blob, streamId: number, chunkSize: number) => sendJSDataStream(connection, data, streamId, chunkSize);
65+
66+
dispatcher = DotNet.attachDispatcher({
67+
beginInvokeDotNetFromJS: (callId, assemblyName, methodIdentifier, dotNetObjectId, argsJson): void => {
68+
connection.send('BeginInvokeDotNetFromJS', callId ? callId.toString() : null, assemblyName, methodIdentifier, dotNetObjectId || 0, argsJson);
69+
},
70+
endInvokeJSFromDotNet: (asyncHandle, succeeded, argsJson): void => {
71+
connection.send('EndInvokeJSFromDotNet', asyncHandle, succeeded, argsJson);
72+
},
73+
sendByteArray: (id: number, data: Uint8Array): void => {
74+
connection.send('ReceiveByteArray', id, data);
75+
},
76+
});
77+
78+
const initialConnection = await initializeConnection(options, logger, circuit);
79+
const circuitStarted = await circuit.startCircuit(initialConnection);
80+
if (!circuitStarted) {
81+
logger.log(LogLevel.Error, 'Failed to start the circuit.');
82+
return;
83+
}
84+
85+
let disconnectSent = false;
86+
const cleanup = () => {
87+
if (!disconnectSent) {
88+
const data = new FormData();
89+
const circuitId = circuit.circuitId!;
90+
data.append('circuitId', circuitId);
91+
disconnectSent = navigator.sendBeacon('_blazor/disconnect', data);
92+
}
93+
};
94+
95+
Blazor.disconnect = cleanup;
96+
97+
window.addEventListener('unload', cleanup, { capture: false, once: true });
98+
99+
logger.log(LogLevel.Information, 'Blazor server-side application started.');
100+
101+
jsInitializer.invokeAfterStartedCallbacks(Blazor);
102+
}
103+
104+
async function initializeConnection(options: CircuitStartOptions, logger: Logger, circuit: CircuitDescriptor): Promise<HubConnection> {
105+
const hubProtocol = new MessagePackHubProtocol();
106+
(hubProtocol as unknown as { name: string }).name = 'blazorpack';
107+
108+
const connectionBuilder = new HubConnectionBuilder()
109+
.withUrl('_blazor')
110+
.withHubProtocol(hubProtocol);
111+
112+
options.configureSignalR(connectionBuilder);
113+
114+
const newConnection = connectionBuilder.build();
115+
116+
newConnection.on('JS.AttachComponent', (componentId, selector) => attachRootComponentToLogicalElement(0, circuit.resolveElement(selector), componentId, false));
117+
newConnection.on('JS.BeginInvokeJS', dispatcher.beginInvokeJSFromDotNet.bind(dispatcher));
118+
newConnection.on('JS.EndInvokeDotNet', dispatcher.endInvokeDotNetFromJS.bind(dispatcher));
119+
newConnection.on('JS.ReceiveByteArray', dispatcher.receiveByteArray.bind(dispatcher));
120+
121+
newConnection.on('JS.BeginTransmitStream', (streamId: number) => {
122+
const readableStream = new ReadableStream({
123+
start(controller) {
124+
newConnection.stream('SendDotNetStreamToJS', streamId).subscribe({
125+
next: (chunk: Uint8Array) => controller.enqueue(chunk),
126+
complete: () => controller.close(),
127+
error: (err) => controller.error(err),
128+
});
129+
},
130+
});
131+
132+
dispatcher.supplyDotNetStream(streamId, readableStream);
133+
});
134+
135+
const renderQueue = RenderQueue.getOrCreate(logger);
136+
newConnection.on('JS.RenderBatch', (batchId: number, batchData: Uint8Array) => {
137+
logger.log(LogLevel.Debug, `Received render batch with id ${batchId} and ${batchData.byteLength} bytes.`);
138+
renderQueue.processBatch(batchId, batchData, newConnection);
139+
});
140+
141+
newConnection.on('JS.EndLocationChanging', Blazor._internal.navigationManager.endLocationChanging);
142+
143+
newConnection.onclose(error => !renderingFailed && options.reconnectionHandler!.onConnectionDown(options.reconnectionOptions, error));
144+
newConnection.on('JS.Error', error => {
145+
renderingFailed = true;
146+
unhandledError(newConnection, error, logger);
147+
showErrorNotification();
148+
});
149+
150+
try {
151+
await newConnection.start();
152+
connection = newConnection;
153+
} catch (ex: any) {
154+
unhandledError(newConnection, ex as Error, logger);
155+
156+
if (ex.errorType === 'FailedToNegotiateWithServerError') {
157+
// Connection with the server has been interrupted, and we're in the process of reconnecting.
158+
// Throw this exception so it can be handled at the reconnection layer, and don't show the
159+
// error notification.
160+
throw ex;
161+
} else {
162+
showErrorNotification();
163+
}
164+
165+
if (ex.innerErrors) {
166+
if (ex.innerErrors.some(e => e.errorType === 'UnsupportedTransportError' && e.transport === HttpTransportType.WebSockets)) {
167+
logger.log(LogLevel.Error, 'Unable to connect, please ensure you are using an updated browser that supports WebSockets.');
168+
} else if (ex.innerErrors.some(e => e.errorType === 'FailedToStartTransportError' && e.transport === HttpTransportType.WebSockets)) {
169+
logger.log(LogLevel.Error, 'Unable to connect, please ensure WebSockets are available. A VPN or proxy may be blocking the connection.');
170+
} else if (ex.innerErrors.some(e => e.errorType === 'DisabledTransportError' && e.transport === HttpTransportType.LongPolling)) {
171+
logger.log(LogLevel.Error, 'Unable to initiate a SignalR connection to the server. This might be because the server is not configured to support WebSockets. For additional details, visit https://aka.ms/blazor-server-websockets-error.');
172+
}
173+
}
174+
}
175+
176+
// Check if the connection is established using the long polling transport,
177+
// using the `features.inherentKeepAlive` property only present with long polling.
178+
if ((newConnection as any).connection?.features?.inherentKeepAlive) {
179+
logger.log(LogLevel.Warning, 'Failed to connect via WebSockets, using the Long Polling fallback transport. This may be due to a VPN or proxy blocking the connection. To troubleshoot this, visit https://aka.ms/blazor-server-using-fallback-long-polling.');
180+
}
181+
182+
return newConnection;
183+
}
184+
185+
function unhandledError(connection: HubConnection, err: Error, logger: Logger): void {
186+
logger.log(LogLevel.Error, err);
187+
188+
// Disconnect on errors.
189+
//
190+
// Trying to call methods on the connection after its been closed will throw.
191+
if (connection) {
192+
connection.stop();
193+
}
194+
}

0 commit comments

Comments
 (0)