Skip to content

Commit 9e5168a

Browse files
Only use circuits when necessary (#9)
* Begin defining blazor.united.js * Factoring out common parts of the server startup logic * Only start a circuit when necessary
1 parent 46bd83e commit 9e5168a

File tree

8 files changed

+245
-188
lines changed

8 files changed

+245
-188
lines changed

src/Components/Samples/BlazorUnitedApp/Shared/MainLayout.razor

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,6 @@
3333
<a class="dismiss">X</a>
3434
</div>
3535

36-
<script src="_framework/blazor.server.js" suppress-error="BL9992"></script>
36+
<script src="_framework/blazor.united.js" suppress-error="BL9992"></script>
3737
</body>
3838
</html>

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,9 +99,12 @@
9999

100100
<PropertyGroup>
101101
<BlazorServerJSFilename>blazor.server.js</BlazorServerJSFilename>
102+
<BlazorUnitedJSFilename>blazor.united.js</BlazorUnitedJSFilename>
102103
<!-- Microsoft.AspNetCore.Components.Web.JS.npmproj always capitalizes the directory name. -->
103104
<BlazorServerJSFile Condition=" '$(Configuration)' == 'Debug' ">..\..\Web.JS\dist\Debug\$(BlazorServerJSFilename)</BlazorServerJSFile>
104105
<BlazorServerJSFile Condition=" '$(Configuration)' != 'Debug' ">..\..\Web.JS\dist\Release\$(BlazorServerJSFilename)</BlazorServerJSFile>
106+
<BlazorUnitedJSFile Condition=" '$(Configuration)' == 'Debug' ">..\..\Web.JS\dist\Debug\$(BlazorUnitedJSFilename)</BlazorUnitedJSFile>
107+
<BlazorUnitedJSFile Condition=" '$(Configuration)' != 'Debug' ">..\..\Web.JS\dist\Release\$(BlazorUnitedJSFilename)</BlazorUnitedJSFile>
105108
</PropertyGroup>
106109

107110
<!-- blazor.server.js should exist after Microsoft.AspNetCore.Components.Web.JS.npmproj builds. Fall back if not. -->
@@ -115,7 +118,9 @@
115118
<Target Name="_AddEmbeddedBlazor" AfterTargets="_CheckBlazorServerJSPath">
116119
<ItemGroup>
117120
<EmbeddedResource Include="$(BlazorServerJSFile)" LogicalName="_framework/$(BlazorServerJSFilename)" />
121+
<EmbeddedResource Include="$(BlazorUnitedJSFile)" LogicalName="_framework/$(BlazorUnitedJSFilename)" />
118122
<EmbeddedResource Include="$(BlazorServerJSFile).map" LogicalName="_framework/$(BlazorServerJSFilename).map" Condition="Exists('$(BlazorServerJSFile).map')" />
123+
<EmbeddedResource Include="$(BlazorUnitedJSFile).map" LogicalName="_framework/$(BlazorUnitedJSFilename).map" Condition="Exists('$(BlazorUnitedJSFile).map')" />
119124
</ItemGroup>
120125
</Target>
121126
</Project>
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
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 { discoverComponents, 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+
23+
export async function startCircuit(userOptions?: Partial<CircuitStartOptions>): Promise<void> {
24+
// Establish options to be used
25+
const options = resolveOptions(userOptions);
26+
const jsInitializer = await fetchAndInvokeInitializers(options);
27+
28+
const logger = new ConsoleLogger(options.logLevel);
29+
30+
Blazor.reconnect = async (existingConnection?: HubConnection): Promise<boolean> => {
31+
if (renderingFailed) {
32+
// We can't reconnect after a failure, so exit early.
33+
return false;
34+
}
35+
36+
const reconnection = existingConnection || await initializeConnection(options, logger, circuit);
37+
if (!(await circuit.reconnect(reconnection))) {
38+
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.');
39+
return false;
40+
}
41+
42+
options.reconnectionHandler!.onConnectionUp();
43+
44+
return true;
45+
};
46+
Blazor.defaultReconnectionHandler = new DefaultReconnectionHandler(logger);
47+
48+
options.reconnectionHandler = options.reconnectionHandler || Blazor.defaultReconnectionHandler;
49+
logger.log(LogLevel.Information, 'Starting up Blazor server-side application.');
50+
51+
const components = discoverComponents(document, 'server') as ServerComponentDescriptor[];
52+
const appState = discoverPersistedState(document);
53+
const circuit = new CircuitDescriptor(components, appState || '');
54+
55+
// Configure navigation via SignalR
56+
Blazor._internal.navigationManager.listenForNavigationEvents((uri: string, state: string | undefined, intercepted: boolean): Promise<void> => {
57+
return connection.send('OnLocationChanged', uri, state, intercepted);
58+
}, (callId: number, uri: string, state: string | undefined, intercepted: boolean): Promise<void> => {
59+
return connection.send('OnLocationChanging', callId, uri, state, intercepted);
60+
});
61+
62+
Blazor._internal.forceCloseConnection = () => connection.stop();
63+
Blazor._internal.sendJSDataStream = (data: ArrayBufferView | Blob, streamId: number, chunkSize: number) => sendJSDataStream(connection, data, streamId, chunkSize);
64+
65+
const initialConnection = await initializeConnection(options, logger, circuit);
66+
const circuitStarted = await circuit.startCircuit(initialConnection);
67+
if (!circuitStarted) {
68+
logger.log(LogLevel.Error, 'Failed to start the circuit.');
69+
return;
70+
}
71+
72+
let disconnectSent = false;
73+
const cleanup = () => {
74+
if (!disconnectSent) {
75+
const data = new FormData();
76+
const circuitId = circuit.circuitId!;
77+
data.append('circuitId', circuitId);
78+
disconnectSent = navigator.sendBeacon('_blazor/disconnect', data);
79+
}
80+
};
81+
82+
Blazor.disconnect = cleanup;
83+
84+
window.addEventListener('unload', cleanup, { capture: false, once: true });
85+
86+
logger.log(LogLevel.Information, 'Blazor server-side application started.');
87+
88+
jsInitializer.invokeAfterStartedCallbacks(Blazor);
89+
}
90+
91+
async function initializeConnection(options: CircuitStartOptions, logger: Logger, circuit: CircuitDescriptor): Promise<HubConnection> {
92+
const hubProtocol = new MessagePackHubProtocol();
93+
(hubProtocol as unknown as { name: string }).name = 'blazorpack';
94+
95+
const connectionBuilder = new HubConnectionBuilder()
96+
.withUrl('_blazor')
97+
.withHubProtocol(hubProtocol);
98+
99+
options.configureSignalR(connectionBuilder);
100+
101+
const newConnection = connectionBuilder.build();
102+
103+
newConnection.on('JS.AttachComponent', (componentId, selector) => attachRootComponentToLogicalElement(0, circuit.resolveElement(selector), componentId, false));
104+
newConnection.on('JS.BeginInvokeJS', DotNet.jsCallDispatcher.beginInvokeJSFromDotNet);
105+
newConnection.on('JS.EndInvokeDotNet', DotNet.jsCallDispatcher.endInvokeDotNetFromJS);
106+
newConnection.on('JS.ReceiveByteArray', DotNet.jsCallDispatcher.receiveByteArray);
107+
108+
newConnection.on('JS.BeginTransmitStream', (streamId: number) => {
109+
const readableStream = new ReadableStream({
110+
start(controller) {
111+
newConnection.stream('SendDotNetStreamToJS', streamId).subscribe({
112+
next: (chunk: Uint8Array) => controller.enqueue(chunk),
113+
complete: () => controller.close(),
114+
error: (err) => controller.error(err),
115+
});
116+
},
117+
});
118+
119+
DotNet.jsCallDispatcher.supplyDotNetStream(streamId, readableStream);
120+
});
121+
122+
const renderQueue = RenderQueue.getOrCreate(logger);
123+
newConnection.on('JS.RenderBatch', (batchId: number, batchData: Uint8Array) => {
124+
logger.log(LogLevel.Debug, `Received render batch with id ${batchId} and ${batchData.byteLength} bytes.`);
125+
renderQueue.processBatch(batchId, batchData, newConnection);
126+
});
127+
128+
newConnection.on('JS.EndLocationChanging', Blazor._internal.navigationManager.endLocationChanging);
129+
130+
newConnection.onclose(error => !renderingFailed && options.reconnectionHandler!.onConnectionDown(options.reconnectionOptions, error));
131+
newConnection.on('JS.Error', error => {
132+
renderingFailed = true;
133+
unhandledError(newConnection, error, logger);
134+
showErrorNotification();
135+
});
136+
137+
try {
138+
await newConnection.start();
139+
connection = newConnection;
140+
} catch (ex: any) {
141+
unhandledError(newConnection, ex as Error, logger);
142+
143+
if (ex.errorType === 'FailedToNegotiateWithServerError') {
144+
// Connection with the server has been interrupted, and we're in the process of reconnecting.
145+
// Throw this exception so it can be handled at the reconnection layer, and don't show the
146+
// error notification.
147+
throw ex;
148+
} else {
149+
showErrorNotification();
150+
}
151+
152+
if (ex.innerErrors) {
153+
if (ex.innerErrors.some(e => e.errorType === 'UnsupportedTransportError' && e.transport === HttpTransportType.WebSockets)) {
154+
logger.log(LogLevel.Error, 'Unable to connect, please ensure you are using an updated browser that supports WebSockets.');
155+
} else if (ex.innerErrors.some(e => e.errorType === 'FailedToStartTransportError' && e.transport === HttpTransportType.WebSockets)) {
156+
logger.log(LogLevel.Error, 'Unable to connect, please ensure WebSockets are available. A VPN or proxy may be blocking the connection.');
157+
} else if (ex.innerErrors.some(e => e.errorType === 'DisabledTransportError' && e.transport === HttpTransportType.LongPolling)) {
158+
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.');
159+
}
160+
}
161+
}
162+
163+
// Check if the connection is established using the long polling transport,
164+
// using the `features.inherentKeepAlive` property only present with long polling.
165+
if ((newConnection as any).connection?.features?.inherentKeepAlive) {
166+
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.');
167+
}
168+
169+
DotNet.attachDispatcher({
170+
beginInvokeDotNetFromJS: (callId, assemblyName, methodIdentifier, dotNetObjectId, argsJson): void => {
171+
newConnection.send('BeginInvokeDotNetFromJS', callId ? callId.toString() : null, assemblyName, methodIdentifier, dotNetObjectId || 0, argsJson);
172+
},
173+
endInvokeJSFromDotNet: (asyncHandle, succeeded, argsJson): void => {
174+
newConnection.send('EndInvokeJSFromDotNet', asyncHandle, succeeded, argsJson);
175+
},
176+
sendByteArray: (id: number, data: Uint8Array): void => {
177+
newConnection.send('ReceiveByteArray', id, data);
178+
},
179+
});
180+
181+
return newConnection;
182+
}
183+
184+
function unhandledError(connection: HubConnection, err: Error, logger: Logger): void {
185+
logger.log(LogLevel.Error, err);
186+
187+
// Disconnect on errors.
188+
//
189+
// Trying to call methods on the connection after its been closed will throw.
190+
if (connection) {
191+
connection.stop();
192+
}
193+
}

0 commit comments

Comments
 (0)