[Components] Avoid creating two connections when resuming circuits
* Fix double connection bug * Fix broken tests * Add test to detect two connections * clean up tests * Fix test bug * Isolate duplicate connection tests
This commit is contained in:
parent
722a34cd56
commit
57940a23aa
|
|
@ -14,6 +14,7 @@ module.exports = {
|
|||
// e.g. "@typescript-eslint/explicit-function-return-type": "off",
|
||||
"@typescript-eslint/indent": ["error", 2],
|
||||
"@typescript-eslint/no-use-before-define": [ "off" ],
|
||||
"@typescript-eslint/no-unused-vars": ["error", { "varsIgnorePattern": "^_", "argsIgnorePattern": "^_" }],
|
||||
"no-var": "error",
|
||||
"prefer-const": "error",
|
||||
"quotes": ["error", "single", { "avoidEscape": true }],
|
||||
|
|
|
|||
|
|
@ -13,26 +13,34 @@ import { discoverPrerenderedCircuits, startCircuit } from './Platform/Circuits/C
|
|||
|
||||
type SignalRBuilder = (builder: signalR.HubConnectionBuilder) => void;
|
||||
interface BlazorOptions {
|
||||
configureSignalR?: SignalRBuilder,
|
||||
};
|
||||
configureSignalR: SignalRBuilder;
|
||||
logLevel: LogLevel;
|
||||
}
|
||||
|
||||
let renderingFailed = false;
|
||||
let started = false;
|
||||
|
||||
async function boot(options?: BlazorOptions): Promise<void> {
|
||||
async function boot(userOptions?: Partial<BlazorOptions>): Promise<void> {
|
||||
|
||||
if (started) {
|
||||
throw new Error('Blazor has already started.');
|
||||
}
|
||||
started = true;
|
||||
|
||||
const defaultOptions: BlazorOptions = {
|
||||
configureSignalR: (_) => { },
|
||||
logLevel: LogLevel.Warning,
|
||||
};
|
||||
|
||||
const options: BlazorOptions = { ...defaultOptions, ...userOptions };
|
||||
|
||||
// For development.
|
||||
// Simply put a break point here and modify the log level during
|
||||
// development to get traces.
|
||||
// In the future we will allow for users to configure this.
|
||||
const logger = new ConsoleLogger(LogLevel.Error);
|
||||
const logger = new ConsoleLogger(options.logLevel);
|
||||
|
||||
logger.log(LogLevel.Information, 'Booting blazor.');
|
||||
logger.log(LogLevel.Information, 'Starting up blazor server-side application.');
|
||||
|
||||
const circuitHandlers: CircuitHandler[] = [new AutoReconnectCircuitHandler(logger)];
|
||||
window['Blazor'].circuitHandlers = circuitHandlers;
|
||||
|
|
@ -43,8 +51,7 @@ async function boot(options?: BlazorOptions): Promise<void> {
|
|||
});
|
||||
|
||||
// pass options.configureSignalR to configure the signalR.HubConnectionBuilder
|
||||
const configureSignalR = (options && options.configureSignalR) || null;
|
||||
const initialConnection = await initializeConnection(configureSignalR, circuitHandlers, logger);
|
||||
const initialConnection = await initializeConnection(options, circuitHandlers, logger);
|
||||
|
||||
const circuits = discoverPrerenderedCircuits(document);
|
||||
for (let i = 0; i < circuits.length; i++) {
|
||||
|
|
@ -64,12 +71,12 @@ async function boot(options?: BlazorOptions): Promise<void> {
|
|||
logger.log(LogLevel.Information, 'No preregistered components to render.');
|
||||
}
|
||||
|
||||
const reconnect = async (): Promise<boolean> => {
|
||||
const reconnect = async (existingConnection?: signalR.HubConnection): Promise<boolean> => {
|
||||
if (renderingFailed) {
|
||||
// We can't reconnect after a failure, so exit early.
|
||||
return false;
|
||||
}
|
||||
const reconnection = await initializeConnection(configureSignalR, circuitHandlers, logger);
|
||||
const reconnection = existingConnection || await initializeConnection(options, circuitHandlers, logger);
|
||||
const results = await Promise.all(circuits.map(circuit => circuit.reconnect(reconnection)));
|
||||
|
||||
if (reconnectionFailed(results)) {
|
||||
|
|
@ -82,7 +89,7 @@ async function boot(options?: BlazorOptions): Promise<void> {
|
|||
|
||||
window['Blazor'].reconnect = reconnect;
|
||||
|
||||
const reconnectTask = reconnect();
|
||||
const reconnectTask = reconnect(initialConnection);
|
||||
|
||||
if (circuit) {
|
||||
circuits.push(circuit);
|
||||
|
|
@ -90,29 +97,29 @@ async function boot(options?: BlazorOptions): Promise<void> {
|
|||
|
||||
await reconnectTask;
|
||||
|
||||
logger.log(LogLevel.Information, 'Blazor server-side application started.');
|
||||
|
||||
function reconnectionFailed(results: boolean[]): boolean {
|
||||
return !results.reduce((current, next) => current && next, true);
|
||||
}
|
||||
}
|
||||
|
||||
async function initializeConnection(configureSignalR: SignalRBuilder | null, circuitHandlers: CircuitHandler[], logger: ILogger): Promise<signalR.HubConnection> {
|
||||
async function initializeConnection(options: Required<BlazorOptions>, circuitHandlers: CircuitHandler[], logger: ILogger): Promise<signalR.HubConnection> {
|
||||
|
||||
const hubProtocol = new MessagePackHubProtocol();
|
||||
(hubProtocol as any).name = 'blazorpack';
|
||||
(hubProtocol as unknown as { name: string }).name = 'blazorpack';
|
||||
|
||||
const connectionBuilder = new signalR.HubConnectionBuilder()
|
||||
.withUrl('_blazor')
|
||||
.withHubProtocol(hubProtocol);
|
||||
|
||||
if (configureSignalR) {
|
||||
configureSignalR(connectionBuilder);
|
||||
}
|
||||
options.configureSignalR(connectionBuilder);
|
||||
|
||||
const connection = connectionBuilder.build();
|
||||
|
||||
connection.on('JS.BeginInvokeJS', DotNet.jsCallDispatcher.beginInvokeJSFromDotNet);
|
||||
connection.on('JS.RenderBatch', (browserRendererId: number, batchId: number, batchData: Uint8Array) => {
|
||||
logger.log(LogLevel.Information, `Received render batch for ${browserRendererId} with id ${batchId} and ${batchData.byteLength} bytes.`);
|
||||
logger.log(LogLevel.Debug, `Received render batch for ${browserRendererId} with id ${batchId} and ${batchData.byteLength} bytes.`);
|
||||
|
||||
const queue = RenderQueue.getOrCreateQueue(browserRendererId, logger);
|
||||
|
||||
|
|
|
|||
|
|
@ -28,18 +28,18 @@ export default class RenderQueue {
|
|||
|
||||
public processBatch(receivedBatchId: number, batchData: Uint8Array, connection: HubConnection): void {
|
||||
if (receivedBatchId < this.nextBatchId) {
|
||||
this.logger.log(LogLevel.Information, `Batch ${receivedBatchId} already processed. Waiting for batch ${this.nextBatchId}.`);
|
||||
this.logger.log(LogLevel.Debug, `Batch ${receivedBatchId} already processed. Waiting for batch ${this.nextBatchId}.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (receivedBatchId > this.nextBatchId) {
|
||||
this.logger.log(LogLevel.Information, `Waiting for batch ${this.nextBatchId}. Batch ${receivedBatchId} not processed.`);
|
||||
this.logger.log(LogLevel.Debug, `Waiting for batch ${this.nextBatchId}. Batch ${receivedBatchId} not processed.`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.nextBatchId++;
|
||||
this.logger.log(LogLevel.Information, `Applying batch ${receivedBatchId}.`);
|
||||
this.logger.log(LogLevel.Debug, `Applying batch ${receivedBatchId}.`);
|
||||
renderBatch(this.browserRendererId, new OutOfProcessRenderBatch(batchData));
|
||||
this.completeBatch(connection, receivedBatchId);
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -48,8 +48,9 @@ namespace Microsoft.AspNetCore.Components.E2ETests.ServerExecutionTests
|
|||
Browser.Equal("No value yet", () => Browser.FindElement(By.Id("val-get-by-interop")).Text);
|
||||
Browser.Equal(string.Empty, () => Browser.FindElement(By.Id("val-set-by-interop")).GetAttribute("value"));
|
||||
|
||||
// Once connected, we can
|
||||
BeginInteractivity();
|
||||
|
||||
// Once connected, we can
|
||||
Browser.Equal("Hello from interop call", () => Browser.FindElement(By.Id("val-get-by-interop")).Text);
|
||||
Browser.Equal("Hello from interop call", () => Browser.FindElement(By.Id("val-set-by-interop")).GetAttribute("value"));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure;
|
||||
|
|
@ -26,14 +28,23 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
_serverFixture.BuildWebHostMethod = ComponentsApp.Server.Program.BuildWebHost;
|
||||
}
|
||||
|
||||
public DateTime LastLogTimeStamp { get; set; } = DateTime.MinValue;
|
||||
|
||||
public override async Task InitializeAsync()
|
||||
{
|
||||
await base.InitializeAsync();
|
||||
Navigate("/", noReload: false);
|
||||
Browser.True(() => Browser.Manage().Logs.GetLog(LogType.Browser)
|
||||
.Any(l => l.Level == LogLevel.Info && l.Message.Contains("blazorpack")));
|
||||
|
||||
// Capture the last log timestamp so that we can filter logs when we
|
||||
// check for duplicate connections.
|
||||
var lastLog = Browser.Manage().Logs.GetLog(LogType.Browser).LastOrDefault();
|
||||
if (lastLog != null)
|
||||
{
|
||||
LastLogTimeStamp = lastLog.Timestamp;
|
||||
}
|
||||
|
||||
Navigate("/", noReload: false);
|
||||
Browser.True(() => ((IJavaScriptExecutor)Browser)
|
||||
.ExecuteScript("return window['__aspnetcore__testing__blazor__started__'];") == null ? false : true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -42,6 +53,18 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
|||
Assert.Equal("Razor Components", Browser.Title);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DoesNotStartTwoConnections()
|
||||
{
|
||||
Browser.True(() =>
|
||||
{
|
||||
var logs = Browser.Manage().Logs.GetLog(LogType.Browser).ToArray();
|
||||
var curatedLogs = logs.Where(l => l.Timestamp > LastLogTimeStamp);
|
||||
|
||||
return curatedLogs.Count(e => e.Message.Contains("blazorpack")) == 1;
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HasHeading()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -17,4 +17,18 @@
|
|||
|
||||
@functions {
|
||||
int count;
|
||||
bool firstRender = false;
|
||||
protected override Task OnAfterRenderAsync()
|
||||
{
|
||||
if (!firstRender)
|
||||
{
|
||||
firstRender = true;
|
||||
|
||||
// We need to queue another render when we connect, otherwise the
|
||||
// browser won't see anything.
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
@page
|
||||
@page
|
||||
@using ComponentsApp.App
|
||||
|
||||
<!DOCTYPE html>
|
||||
|
|
@ -20,7 +20,10 @@
|
|||
Blazor.start({
|
||||
configureSignalR: function (builder) {
|
||||
builder.configureLogging(2); // LogLevel.Information
|
||||
}
|
||||
},
|
||||
logLevel: 2 // LogLevel.Information
|
||||
}).then(function () {
|
||||
window['__aspnetcore__testing__blazor__started__'] = true;
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
@page
|
||||
@using BasicTestApp.RouterTest
|
||||
@using Microsoft.AspNetCore.Mvc.ViewFeatures
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
|
|
@ -16,7 +15,7 @@
|
|||
*@
|
||||
<hr />
|
||||
|
||||
<button id="load-boot-script" onclick="Blazor.start()">Load boot script</button>
|
||||
<button id="load-boot-script" onclick="start()">Load boot script</button>
|
||||
|
||||
<script src="_framework/components.server.js" autostart="false"></script>
|
||||
<script>
|
||||
|
|
@ -24,6 +23,12 @@
|
|||
function setElementValue(element, newValue) {
|
||||
element.value = newValue;
|
||||
return element.value;
|
||||
}
|
||||
|
||||
function start() {
|
||||
Blazor.start({
|
||||
logLevel: 1 // LogLevel.Debug
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ namespace TestServer
|
|||
subdirApp.UseEndpoints(endpoints =>
|
||||
{
|
||||
endpoints.MapFallbackToPage("/PrerenderedHost");
|
||||
endpoints.MapComponentHub<TestRouter>("app");
|
||||
endpoints.MapComponentHub();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue