-
Notifications
You must be signed in to change notification settings - Fork 10.3k
Enable Blazor Server and WebAssembly components to run in the same document #48294
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
Changes from all commits
73b3eea
743704a
7bfd3ef
55dbbfa
c60cede
11f1af2
2afbd46
6756170
f678b4a
62ead8a
af94b2a
1915288
83381be
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -148,4 +148,4 @@ | |
"src\\WebEncoders\\src\\Microsoft.Extensions.WebEncoders.csproj" | ||
] | ||
} | ||
} | ||
} |
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,194 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
|
||
import { DotNet } from '@microsoft/dotnet-js-interop'; | ||
import { Blazor } from './GlobalExports'; | ||
import { HubConnectionBuilder, HubConnection, HttpTransportType } from '@microsoft/signalr'; | ||
import { MessagePackHubProtocol } from '@microsoft/signalr-protocol-msgpack'; | ||
import { showErrorNotification } from './BootErrors'; | ||
import { RenderQueue } from './Platform/Circuits/RenderQueue'; | ||
import { ConsoleLogger } from './Platform/Logging/Loggers'; | ||
import { LogLevel, Logger } from './Platform/Logging/Logger'; | ||
import { CircuitDescriptor } from './Platform/Circuits/CircuitManager'; | ||
import { resolveOptions, CircuitStartOptions } from './Platform/Circuits/CircuitStartOptions'; | ||
import { DefaultReconnectionHandler } from './Platform/Circuits/DefaultReconnectionHandler'; | ||
import { attachRootComponentToLogicalElement } from './Rendering/Renderer'; | ||
import { discoverPersistedState, ServerComponentDescriptor } from './Services/ComponentDescriptorDiscovery'; | ||
import { sendJSDataStream } from './Platform/Circuits/CircuitStreamingInterop'; | ||
import { fetchAndInvokeInitializers } from './JSInitializers/JSInitializers.Server'; | ||
|
||
let renderingFailed = false; | ||
let connection: HubConnection; | ||
let circuit: CircuitDescriptor; | ||
let dispatcher: DotNet.ICallDispatcher; | ||
|
||
export async function startCircuit(userOptions?: Partial<CircuitStartOptions>, components?: ServerComponentDescriptor[]): Promise<void> { | ||
// Establish options to be used | ||
const options = resolveOptions(userOptions); | ||
const jsInitializer = await fetchAndInvokeInitializers(options); | ||
|
||
const logger = new ConsoleLogger(options.logLevel); | ||
|
||
Blazor.reconnect = async (existingConnection?: HubConnection): Promise<boolean> => { | ||
if (renderingFailed) { | ||
// We can't reconnect after a failure, so exit early. | ||
return false; | ||
} | ||
|
||
const reconnection = existingConnection || await initializeConnection(options, logger, circuit); | ||
if (!(await circuit.reconnect(reconnection))) { | ||
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.'); | ||
return false; | ||
} | ||
|
||
options.reconnectionHandler!.onConnectionUp(); | ||
|
||
return true; | ||
}; | ||
Blazor.defaultReconnectionHandler = new DefaultReconnectionHandler(logger); | ||
|
||
options.reconnectionHandler = options.reconnectionHandler || Blazor.defaultReconnectionHandler; | ||
logger.log(LogLevel.Information, 'Starting up Blazor server-side application.'); | ||
|
||
const appState = discoverPersistedState(document); | ||
circuit = new CircuitDescriptor(components || [], appState || ''); | ||
|
||
// Configure navigation via SignalR | ||
Blazor._internal.navigationManager.listenForNavigationEvents((uri: string, state: string | undefined, intercepted: boolean): Promise<void> => { | ||
return connection.send('OnLocationChanged', uri, state, intercepted); | ||
}, (callId: number, uri: string, state: string | undefined, intercepted: boolean): Promise<void> => { | ||
return connection.send('OnLocationChanging', callId, uri, state, intercepted); | ||
}); | ||
|
||
Blazor._internal.forceCloseConnection = () => connection.stop(); | ||
Blazor._internal.sendJSDataStream = (data: ArrayBufferView | Blob, streamId: number, chunkSize: number) => sendJSDataStream(connection, data, streamId, chunkSize); | ||
|
||
dispatcher = DotNet.attachDispatcher({ | ||
beginInvokeDotNetFromJS: (callId, assemblyName, methodIdentifier, dotNetObjectId, argsJson): void => { | ||
connection.send('BeginInvokeDotNetFromJS', callId ? callId.toString() : null, assemblyName, methodIdentifier, dotNetObjectId || 0, argsJson); | ||
}, | ||
endInvokeJSFromDotNet: (asyncHandle, succeeded, argsJson): void => { | ||
connection.send('EndInvokeJSFromDotNet', asyncHandle, succeeded, argsJson); | ||
}, | ||
sendByteArray: (id: number, data: Uint8Array): void => { | ||
connection.send('ReceiveByteArray', id, data); | ||
}, | ||
}); | ||
Comment on lines
+66
to
+76
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could we make this a bit more idiomatic? In this multi dispatching world I would have expected for us to new up a dispatcher instance and configure it. With a server implementation and a webassembly implementation. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just so I'm understanding correctly, is the only change you're suggesting that we change it from There already is a server and client implementation with the way things are currently set up. I guess the one advantage of keeping the |
||
|
||
const initialConnection = await initializeConnection(options, logger, circuit); | ||
const circuitStarted = await circuit.startCircuit(initialConnection); | ||
if (!circuitStarted) { | ||
logger.log(LogLevel.Error, 'Failed to start the circuit.'); | ||
return; | ||
} | ||
|
||
let disconnectSent = false; | ||
const cleanup = () => { | ||
if (!disconnectSent) { | ||
const data = new FormData(); | ||
const circuitId = circuit.circuitId!; | ||
data.append('circuitId', circuitId); | ||
disconnectSent = navigator.sendBeacon('_blazor/disconnect', data); | ||
} | ||
}; | ||
|
||
Blazor.disconnect = cleanup; | ||
|
||
window.addEventListener('unload', cleanup, { capture: false, once: true }); | ||
|
||
logger.log(LogLevel.Information, 'Blazor server-side application started.'); | ||
|
||
jsInitializer.invokeAfterStartedCallbacks(Blazor); | ||
} | ||
|
||
async function initializeConnection(options: CircuitStartOptions, logger: Logger, circuit: CircuitDescriptor): Promise<HubConnection> { | ||
const hubProtocol = new MessagePackHubProtocol(); | ||
(hubProtocol as unknown as { name: string }).name = 'blazorpack'; | ||
|
||
const connectionBuilder = new HubConnectionBuilder() | ||
.withUrl('_blazor') | ||
.withHubProtocol(hubProtocol); | ||
|
||
options.configureSignalR(connectionBuilder); | ||
|
||
const newConnection = connectionBuilder.build(); | ||
|
||
newConnection.on('JS.AttachComponent', (componentId, selector) => attachRootComponentToLogicalElement(0, circuit.resolveElement(selector), componentId, false)); | ||
newConnection.on('JS.BeginInvokeJS', dispatcher.beginInvokeJSFromDotNet.bind(dispatcher)); | ||
newConnection.on('JS.EndInvokeDotNet', dispatcher.endInvokeDotNetFromJS.bind(dispatcher)); | ||
newConnection.on('JS.ReceiveByteArray', dispatcher.receiveByteArray.bind(dispatcher)); | ||
|
||
newConnection.on('JS.BeginTransmitStream', (streamId: number) => { | ||
const readableStream = new ReadableStream({ | ||
start(controller) { | ||
newConnection.stream('SendDotNetStreamToJS', streamId).subscribe({ | ||
next: (chunk: Uint8Array) => controller.enqueue(chunk), | ||
complete: () => controller.close(), | ||
error: (err) => controller.error(err), | ||
}); | ||
}, | ||
}); | ||
|
||
dispatcher.supplyDotNetStream(streamId, readableStream); | ||
}); | ||
|
||
const renderQueue = RenderQueue.getOrCreate(logger); | ||
newConnection.on('JS.RenderBatch', (batchId: number, batchData: Uint8Array) => { | ||
logger.log(LogLevel.Debug, `Received render batch with id ${batchId} and ${batchData.byteLength} bytes.`); | ||
renderQueue.processBatch(batchId, batchData, newConnection); | ||
}); | ||
|
||
newConnection.on('JS.EndLocationChanging', Blazor._internal.navigationManager.endLocationChanging); | ||
|
||
newConnection.onclose(error => !renderingFailed && options.reconnectionHandler!.onConnectionDown(options.reconnectionOptions, error)); | ||
newConnection.on('JS.Error', error => { | ||
renderingFailed = true; | ||
unhandledError(newConnection, error, logger); | ||
showErrorNotification(); | ||
}); | ||
|
||
try { | ||
await newConnection.start(); | ||
connection = newConnection; | ||
} catch (ex: any) { | ||
unhandledError(newConnection, ex as Error, logger); | ||
|
||
if (ex.errorType === 'FailedToNegotiateWithServerError') { | ||
// Connection with the server has been interrupted, and we're in the process of reconnecting. | ||
// Throw this exception so it can be handled at the reconnection layer, and don't show the | ||
// error notification. | ||
throw ex; | ||
} else { | ||
showErrorNotification(); | ||
} | ||
|
||
if (ex.innerErrors) { | ||
if (ex.innerErrors.some(e => e.errorType === 'UnsupportedTransportError' && e.transport === HttpTransportType.WebSockets)) { | ||
logger.log(LogLevel.Error, 'Unable to connect, please ensure you are using an updated browser that supports WebSockets.'); | ||
} else if (ex.innerErrors.some(e => e.errorType === 'FailedToStartTransportError' && e.transport === HttpTransportType.WebSockets)) { | ||
logger.log(LogLevel.Error, 'Unable to connect, please ensure WebSockets are available. A VPN or proxy may be blocking the connection.'); | ||
} else if (ex.innerErrors.some(e => e.errorType === 'DisabledTransportError' && e.transport === HttpTransportType.LongPolling)) { | ||
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.'); | ||
} | ||
} | ||
} | ||
|
||
// Check if the connection is established using the long polling transport, | ||
// using the `features.inherentKeepAlive` property only present with long polling. | ||
if ((newConnection as any).connection?.features?.inherentKeepAlive) { | ||
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.'); | ||
} | ||
|
||
return newConnection; | ||
} | ||
|
||
function unhandledError(connection: HubConnection, err: Error, logger: Logger): void { | ||
logger.log(LogLevel.Error, err); | ||
|
||
// Disconnect on errors. | ||
// | ||
// Trying to call methods on the connection after its been closed will throw. | ||
if (connection) { | ||
connection.stop(); | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.