Skip to content

Commit

Permalink
Only use circuits when necessary (#9)
Browse files Browse the repository at this point in the history
* Begin defining blazor.united.js

* Factoring out common parts of the server startup logic

* Only start a circuit when necessary
  • Loading branch information
SteveSandersonMS authored Dec 7, 2022
1 parent 46bd83e commit 9e5168a
Show file tree
Hide file tree
Showing 8 changed files with 245 additions and 188 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,6 @@
<a class="dismiss">X</a>
</div>

<script src="_framework/blazor.server.js" suppress-error="BL9992"></script>
<script src="_framework/blazor.united.js" suppress-error="BL9992"></script>
</body>
</html>
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,12 @@

<PropertyGroup>
<BlazorServerJSFilename>blazor.server.js</BlazorServerJSFilename>
<BlazorUnitedJSFilename>blazor.united.js</BlazorUnitedJSFilename>
<!-- Microsoft.AspNetCore.Components.Web.JS.npmproj always capitalizes the directory name. -->
<BlazorServerJSFile Condition=" '$(Configuration)' == 'Debug' ">..\..\Web.JS\dist\Debug\$(BlazorServerJSFilename)</BlazorServerJSFile>
<BlazorServerJSFile Condition=" '$(Configuration)' != 'Debug' ">..\..\Web.JS\dist\Release\$(BlazorServerJSFilename)</BlazorServerJSFile>
<BlazorUnitedJSFile Condition=" '$(Configuration)' == 'Debug' ">..\..\Web.JS\dist\Debug\$(BlazorUnitedJSFilename)</BlazorUnitedJSFile>
<BlazorUnitedJSFile Condition=" '$(Configuration)' != 'Debug' ">..\..\Web.JS\dist\Release\$(BlazorUnitedJSFilename)</BlazorUnitedJSFile>
</PropertyGroup>

<!-- blazor.server.js should exist after Microsoft.AspNetCore.Components.Web.JS.npmproj builds. Fall back if not. -->
Expand All @@ -115,7 +118,9 @@
<Target Name="_AddEmbeddedBlazor" AfterTargets="_CheckBlazorServerJSPath">
<ItemGroup>
<EmbeddedResource Include="$(BlazorServerJSFile)" LogicalName="_framework/$(BlazorServerJSFilename)" />
<EmbeddedResource Include="$(BlazorUnitedJSFile)" LogicalName="_framework/$(BlazorUnitedJSFilename)" />
<EmbeddedResource Include="$(BlazorServerJSFile).map" LogicalName="_framework/$(BlazorServerJSFilename).map" Condition="Exists('$(BlazorServerJSFile).map')" />
<EmbeddedResource Include="$(BlazorUnitedJSFile).map" LogicalName="_framework/$(BlazorUnitedJSFilename).map" Condition="Exists('$(BlazorUnitedJSFile).map')" />
</ItemGroup>
</Target>
</Project>
193 changes: 193 additions & 0 deletions src/Components/Web.JS/src/Boot.Server.Common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
// 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 { discoverComponents, discoverPersistedState, ServerComponentDescriptor } from './Services/ComponentDescriptorDiscovery';
import { sendJSDataStream } from './Platform/Circuits/CircuitStreamingInterop';
import { fetchAndInvokeInitializers } from './JSInitializers/JSInitializers.Server';

let renderingFailed = false;
let connection: HubConnection;

export async function startCircuit(userOptions?: Partial<CircuitStartOptions>): 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 components = discoverComponents(document, 'server') as ServerComponentDescriptor[];
const appState = discoverPersistedState(document);
const 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);

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', DotNet.jsCallDispatcher.beginInvokeJSFromDotNet);
newConnection.on('JS.EndInvokeDotNet', DotNet.jsCallDispatcher.endInvokeDotNetFromJS);
newConnection.on('JS.ReceiveByteArray', DotNet.jsCallDispatcher.receiveByteArray);

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),
});
},
});

DotNet.jsCallDispatcher.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.');
}

DotNet.attachDispatcher({
beginInvokeDotNetFromJS: (callId, assemblyName, methodIdentifier, dotNetObjectId, argsJson): void => {
newConnection.send('BeginInvokeDotNetFromJS', callId ? callId.toString() : null, assemblyName, methodIdentifier, dotNetObjectId || 0, argsJson);
},
endInvokeJSFromDotNet: (asyncHandle, succeeded, argsJson): void => {
newConnection.send('EndInvokeJSFromDotNet', asyncHandle, succeeded, argsJson);
},
sendByteArray: (id: number, data: Uint8Array): void => {
newConnection.send('ReceiveByteArray', id, data);
},
});

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();
}
}
Loading

0 comments on commit 9e5168a

Please sign in to comment.