Skip to content
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

Javiercn/blazor improved circuit is #11376

Closed
wants to merge 25 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions eng/Dependencies.props
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ and are generated based on the last package release.
<LatestPackageReference Include="Microsoft.AspNetCore.Razor.Language" Version="$(MicrosoftAspNetCoreRazorLanguagePackageVersion)" />
<LatestPackageReference Include="Microsoft.AspNetCore.Testing" Version="$(MicrosoftAspNetCoreTestingPackageVersion)" />
<LatestPackageReference Include="Microsoft.Azure.KeyVault" Version="$(MicrosoftAzureKeyVaultPackageVersion)" />
<LatestPackageReference Include="Microsoft.Azure.SignalR" Version="1.1.0-preview1-10426" />
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Included here so that its easier to test with the rest of the repository.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When this is merged, I'm guessing you'll remove this, and ensure the CTI scripts give us adequate coverage of the SignalR Service scenarios?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't hurt CTI, it only puts the reference in our build infrastructure so that we can do <Reference Import="Microsoft.Azure.SignalR" /> in our test projects when we want to test something against Azure SignalR

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is going to be constantly broken while we're still making changes. I'd suggest not doing this right now and wait until we stop making big API changes that break the azure signalr SDK.

<LatestPackageReference Include="Microsoft.Azure.Storage.Blob" Version="$(MicrosoftAzureStorageBlobPackageVersion)" />
<LatestPackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="$(MicrosoftBclAsyncInterfacesPackageVersion)" />
<LatestPackageReference Include="Microsoft.Build.Framework" Version="$(MicrosoftBuildFrameworkPackageVersion)" />
Expand Down
6 changes: 3 additions & 3 deletions src/Components/Browser.JS/dist/Release/blazor.server.js

Large diffs are not rendered by default.

46 changes: 30 additions & 16 deletions src/Components/Browser.JS/src/Boot.Server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { discoverPrerenderedCircuits, startCircuit } from './Platform/Circuits/C
type SignalRBuilder = (builder: signalR.HubConnectionBuilder) => void;
interface BlazorOptions {
configureSignalR: SignalRBuilder;
serviceUrl: string;
logLevel: LogLevel;
}

Expand All @@ -29,6 +30,7 @@ async function boot(userOptions?: Partial<BlazorOptions>): Promise<void> {

const defaultOptions: BlazorOptions = {
configureSignalR: (_) => { },
serviceUrl: '_blazor',
logLevel: LogLevel.Warning,
};

Expand All @@ -51,9 +53,15 @@ async function boot(userOptions?: Partial<BlazorOptions>): Promise<void> {
});

// pass options.configureSignalR to configure the signalR.HubConnectionBuilder
const initialConnection = await initializeConnection(options, circuitHandlers, logger);

const circuits = discoverPrerenderedCircuits(document);
if (circuits.length > 1){
throw new Error('Can\'t have multiple circuits per connection');
}

const circuitId = circuits.length > 0 ? circuits[0].circuitId : await getNewCircuitId(options);

const initialConnection = await initializeConnection(options, circuitHandlers, circuitId, logger);

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Further cleanup can be done to this file, but I'm not going to do it in this PR

for (let i = 0; i < circuits.length; i++) {
const circuit = circuits[i];
for (let j = 0; j < circuit.components.length; j++) {
Expand All @@ -65,18 +73,12 @@ async function boot(userOptions?: Partial<BlazorOptions>): Promise<void> {
// Ensure any embedded resources have been loaded before starting the app
await embeddedResourcesPromise;

const circuit = await startCircuit(initialConnection);

if (!circuit) {
logger.log(LogLevel.Information, 'No preregistered components to render.');
}

const reconnect = async (existingConnection?: signalR.HubConnection): Promise<boolean> => {
if (renderingFailed) {
// We can't reconnect after a failure, so exit early.
return false;
}
const reconnection = existingConnection || await initializeConnection(options, circuitHandlers, logger);
const reconnection = existingConnection || await initializeConnection(options, circuitHandlers, circuitId, logger);
const results = await Promise.all(circuits.map(circuit => circuit.reconnect(reconnection)));

if (reconnectionFailed(results)) {
Expand All @@ -89,13 +91,15 @@ async function boot(userOptions?: Partial<BlazorOptions>): Promise<void> {

window['Blazor'].reconnect = reconnect;

const reconnectTask = reconnect(initialConnection);
await reconnect(initialConnection);

if (circuit) {
circuits.push(circuit);
}
// We render any additional component after all prerendered components have
// re-stablished the connection with the circuit.
const renderedComponents = await startCircuit(circuitId, initialConnection);

await reconnectTask;
if (!renderedComponents) {
logger.log(LogLevel.Information, 'No preregistered components to render.');
}
SteveSandersonMS marked this conversation as resolved.
Show resolved Hide resolved

logger.log(LogLevel.Information, 'Blazor server-side application started.');

Expand All @@ -104,13 +108,23 @@ async function boot(userOptions?: Partial<BlazorOptions>): Promise<void> {
}
}

async function initializeConnection(options: Required<BlazorOptions>, circuitHandlers: CircuitHandler[], logger: ILogger): Promise<signalR.HubConnection> {
async function getNewCircuitId(options: BlazorOptions): Promise<string> {
const response = await fetch(`${options.serviceUrl}/start`, {
method: 'POST',
});

const responseBody = await response.json() as { id: string };

return responseBody.id;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the TS-side code, could we rename all references to "circuit ID" to something like circuitIdRequestToken, or whatever its more accurate name now is? Otherwise it's hard to keep track of what aspect of the circuit ID we're sharing with the client.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair enough

}

async function initializeConnection(options: Required<BlazorOptions>, circuitHandlers: CircuitHandler[], circuitId: string, logger: ILogger): Promise<signalR.HubConnection> {

const hubProtocol = new MessagePackHubProtocol();
(hubProtocol as unknown as { name: string }).name = 'blazorpack';

const connectionBuilder = new signalR.HubConnectionBuilder()
.withUrl('_blazor')
.withUrl(`${options.serviceUrl}?circuitId=${circuitId}`)
.withHubProtocol(hubProtocol);

options.configureSignalR(connectionBuilder);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ export function discoverPrerenderedCircuits(document: Document): CircuitDescript
return circuits;
}

export async function startCircuit(connection: signalR.HubConnection): Promise<CircuitDescriptor | undefined> {
const result = await connection.invoke<string>('StartCircuit', uriHelperFunctions.getLocationHref(), uriHelperFunctions.getBaseURI());
export async function startCircuit(circuitId: string, connection: signalR.HubConnection): Promise<CircuitDescriptor | undefined> {
const result = await connection.invoke<string>('StartCircuit', circuitId, uriHelperFunctions.getLocationHref(), uriHelperFunctions.getBaseURI());
if (result) {
return new CircuitDescriptor(result, []);
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
</PropertyGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'netcoreapp3.0'">
<Compile Include="Microsoft.AspNetCore.Components.Server.netcoreapp3.0.cs" />
<Reference Include="Microsoft.AspNetCore.Authentication" />
<Reference Include="Microsoft.AspNetCore.Cors" />
<Reference Include="Microsoft.AspNetCore.Components.Browser" />
<Reference Include="Microsoft.AspNetCore.DataProtection" />
<Reference Include="Microsoft.Extensions.Logging" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ public static partial class ComponentEndpointRouteBuilderExtensions
{
public static Microsoft.AspNetCore.Components.Server.ComponentEndpointConventionBuilder MapBlazorHub(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints) { throw null; }
public static Microsoft.AspNetCore.Components.Server.ComponentEndpointConventionBuilder MapBlazorHub(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, System.Action<Microsoft.AspNetCore.Http.Connections.HttpConnectionDispatcherOptions> configureOptions) { throw null; }
public static Microsoft.AspNetCore.Components.Server.ComponentEndpointConventionBuilder MapBlazorHub(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, string path) { throw null; }
public static Microsoft.AspNetCore.Components.Server.ComponentEndpointConventionBuilder MapBlazorHub(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, string path, System.Action<Microsoft.AspNetCore.Http.Connections.HttpConnectionDispatcherOptions> configureOptions) { throw null; }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As an API review point, we should consider wrapping both the path and HttpConnectionDispatcherOptions into some new type BlazorHubOptions so that we're free to keep adding more options in the future without an exponentially increasing number of overload combinations. Adding a note to #4050

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's fair. I would also hope that at some point this ends up being something on ComponentEndpointConventionBuilder instead of overloads.

public static Microsoft.AspNetCore.Components.Server.ComponentEndpointConventionBuilder MapBlazorHub(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, System.Type type, string selector) { throw null; }
public static Microsoft.AspNetCore.Components.Server.ComponentEndpointConventionBuilder MapBlazorHub(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, System.Type type, string selector, System.Action<Microsoft.AspNetCore.Http.Connections.HttpConnectionDispatcherOptions> configureOptions) { throw null; }
public static Microsoft.AspNetCore.Components.Server.ComponentEndpointConventionBuilder MapBlazorHub(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder endpoints, System.Type componentType, string selector, string path) { throw null; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.SignalR;

Expand All @@ -12,11 +13,11 @@ namespace Microsoft.AspNetCore.Components.Server
/// </summary>
public sealed class ComponentEndpointConventionBuilder : IHubEndpointConventionBuilder
{
private readonly IEndpointConventionBuilder _endpointConventionBuilder;
private readonly IEndpointConventionBuilder[] _endpointConventionBuilders;

internal ComponentEndpointConventionBuilder(IEndpointConventionBuilder endpointConventionBuilder)
internal ComponentEndpointConventionBuilder(params IEndpointConventionBuilder[] endpointConventionBuilders)
{
_endpointConventionBuilder = endpointConventionBuilder;
_endpointConventionBuilders = endpointConventionBuilders;
}

/// <summary>
Expand All @@ -25,7 +26,10 @@ internal ComponentEndpointConventionBuilder(IEndpointConventionBuilder endpointC
/// <param name="convention">The convention to add to the builder.</param>
public void Add(Action<EndpointBuilder> convention)
{
_endpointConventionBuilder.Add(convention);
for (int i = 0; i < _endpointConventionBuilders.Length; i++)
{
_endpointConventionBuilders[i].Add(convention);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Server;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Http.Connections;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.SignalR;
Expand All @@ -30,6 +31,22 @@ public static ComponentEndpointConventionBuilder MapBlazorHub(this IEndpointRout
return endpoints.MapBlazorHub(configureOptions: _ => { });
}

/// <summary>
/// Maps the Blazor <see cref="Hub" /> to the path <paramref name="path"/>.
/// </summary>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/>.</param>
/// <param name="path">The path to map the Blazor <see cref="Hub" />.</param>
/// <returns>The <see cref="ComponentEndpointConventionBuilder"/>.</returns>
public static ComponentEndpointConventionBuilder MapBlazorHub(this IEndpointRouteBuilder endpoints, string path)
{
if (endpoints == null)
{
throw new ArgumentNullException(nameof(endpoints));
}

return endpoints.MapBlazorHub(path, configureOptions: _ => { });
}

/// <summary>
/// Maps the Blazor <see cref="Hub" /> to the default path.
/// </summary>
Expand All @@ -48,7 +65,31 @@ public static ComponentEndpointConventionBuilder MapBlazorHub(this IEndpointRout
throw new ArgumentNullException(nameof(configureOptions));
}

return new ComponentEndpointConventionBuilder(endpoints.MapHub<ComponentHub>(ComponentHub.DefaultPath, configureOptions));
return endpoints.MapBlazorHub(ComponentHub.DefaultPath, configureOptions);
}

/// <summary>
/// Maps the Blazor <see cref="Hub" /> to the path <paramref name="path"/>.
/// </summary>
/// <param name="endpoints">The <see cref="IEndpointRouteBuilder"/>.</param>
/// <param name="path">The path to map the Blazor <see cref="Hub" />.</param>
/// <param name="configureOptions">A callback to configure dispatcher options.</param>
/// <returns>The <see cref="ComponentEndpointConventionBuilder"/>.</returns>
public static ComponentEndpointConventionBuilder MapBlazorHub(this IEndpointRouteBuilder endpoints, string path, Action<HttpConnectionDispatcherOptions> configureOptions)
{
if (endpoints == null)
{
throw new ArgumentNullException(nameof(endpoints));
}

if (configureOptions == null)
{
throw new ArgumentNullException(nameof(configureOptions));
}

return new ComponentEndpointConventionBuilder(
endpoints.MapHub<ComponentHub>(path, configureOptions),
endpoints.MapStartCircuitEndpoint(path + "/start"));
}

/// <summary>
Expand Down Expand Up @@ -328,7 +369,8 @@ public static ComponentEndpointConventionBuilder MapBlazorHub(
throw new ArgumentNullException(nameof(configureOptions));
}

return new ComponentEndpointConventionBuilder(endpoints.MapHub<ComponentHub>(path, configureOptions)).AddComponent(componentType, selector);
return endpoints.MapBlazorHub(path, configureOptions)
.AddComponent(componentType, selector);
}
}
}
1 change: 1 addition & 0 deletions src/Components/Server/src/CircuitOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using Microsoft.AspNetCore.Authorization;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line probably isn't required on its own.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope, this is cleanup.

using Microsoft.AspNetCore.DataProtection;

namespace Microsoft.AspNetCore.Components.Server
Expand Down
2 changes: 1 addition & 1 deletion src/Components/Server/src/Circuits/Circuit.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,6 @@ internal Circuit(CircuitHost circuitHost)
/// <summary>
/// Gets the identifier for the <see cref="Circuit"/>.
/// </summary>
public string Id => _circuitHost.CircuitId;
public string Id => _circuitHost.CircuitId.RequestToken;
}
}
19 changes: 13 additions & 6 deletions src/Components/Server/src/Circuits/CircuitHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public static void SetCurrentCircuitHost(CircuitHost circuitHost)
public event UnhandledExceptionEventHandler UnhandledException;

public CircuitHost(
string circuitId,
CircuitId circuitId,
IServiceScope scope,
CircuitClientProxy client,
RendererRegistry rendererRegistry,
Expand Down Expand Up @@ -80,7 +80,7 @@ public CircuitHost(
Renderer.UnhandledSynchronizationException += SynchronizationContext_UnhandledException;
}

public string CircuitId { get; }
public CircuitId CircuitId { get; }

public Circuit Circuit { get; }

Expand Down Expand Up @@ -140,7 +140,7 @@ internal void SendPendingBatches()
var _ = Renderer.InvokeAsync(() => Renderer.ProcessBufferedRenderBatches());
}

public async Task InitializeAsync(CancellationToken cancellationToken)
public async Task InitializeAsync(CancellationToken cancellationToken, bool existingCircuit)
{
await Renderer.InvokeAsync(async () =>
{
Expand All @@ -149,8 +149,15 @@ await Renderer.InvokeAsync(async () =>
SetCurrentCircuitHost(this);
_initialized = true; // We're ready to accept incoming JSInterop calls from here on

await OnCircuitOpenedAsync(cancellationToken);
await OnConnectionUpAsync(cancellationToken);
if (!existingCircuit)
{
// If the circuit we are using was created during prerendering don't run
// the lifecycle events again at this time.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was this a bug before? I'm surprised we need to change that behavior unless it was a bug before.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am a bit uncertain about the meanings of these lifecycle events now. Previously I would have expected:

  • OnCircuitOpenedAsync to occur during prerendering
  • OnConnectionUpAsync to occur after the client opens the socket connection and is ready for interactivity

... but it seems with this change that there wouldn't be a way to know when the socket connection was opened. Is this right? I'm not sure we've got a formal definition of when these lifecycle events should fire, but now would be a good time to be explicit about it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The way things work now:

Prerendered components or prerendered+non-prerendered

  • Prerender -> Runs on Circuit Opened
  • Connect -> Runs OnConnectionUp
  • StartCircuit -> Renders additional components

Only non prerendered components

  • StartCircuit runs OnCircuitOpenedAsyn -> OnConnectionUpAsync -> RendersAdditional components.

OnConnectionUp always marks when the socket is opened and we're ready to perform JSInterop.

// Circuit opened ran during prerendering
// ConnectionUp will run after the reconnection is completed.
await OnCircuitOpenedAsync(cancellationToken);
await OnConnectionUpAsync(cancellationToken);
}

// We add the root components *after* the circuit is flagged as open.
// That's because AddComponentAsync waits for quiescence, which can take
Expand Down Expand Up @@ -289,7 +296,7 @@ private async Task OnCircuitDownAsync()

public async ValueTask DisposeAsync()
{
Log.DisposingCircuit(_logger, CircuitId);
Log.DisposingCircuit(_logger, CircuitId.RequestToken);

await Renderer.InvokeAsync(async () =>
{
Expand Down
Loading