[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:
Javier Calvarro Nelson 2019-04-04 08:19:05 +02:00 committed by GitHub
parent 722a34cd56
commit 57940a23aa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 82 additions and 28 deletions

View File

@ -14,6 +14,7 @@ module.exports = {
// e.g. "@typescript-eslint/explicit-function-return-type": "off", // e.g. "@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/indent": ["error", 2], "@typescript-eslint/indent": ["error", 2],
"@typescript-eslint/no-use-before-define": [ "off" ], "@typescript-eslint/no-use-before-define": [ "off" ],
"@typescript-eslint/no-unused-vars": ["error", { "varsIgnorePattern": "^_", "argsIgnorePattern": "^_" }],
"no-var": "error", "no-var": "error",
"prefer-const": "error", "prefer-const": "error",
"quotes": ["error", "single", { "avoidEscape": true }], "quotes": ["error", "single", { "avoidEscape": true }],

View File

@ -13,26 +13,34 @@ import { discoverPrerenderedCircuits, startCircuit } from './Platform/Circuits/C
type SignalRBuilder = (builder: signalR.HubConnectionBuilder) => void; type SignalRBuilder = (builder: signalR.HubConnectionBuilder) => void;
interface BlazorOptions { interface BlazorOptions {
configureSignalR?: SignalRBuilder, configureSignalR: SignalRBuilder;
}; logLevel: LogLevel;
}
let renderingFailed = false; let renderingFailed = false;
let started = false; let started = false;
async function boot(options?: BlazorOptions): Promise<void> { async function boot(userOptions?: Partial<BlazorOptions>): Promise<void> {
if (started) { if (started) {
throw new Error('Blazor has already started.'); throw new Error('Blazor has already started.');
} }
started = true; started = true;
const defaultOptions: BlazorOptions = {
configureSignalR: (_) => { },
logLevel: LogLevel.Warning,
};
const options: BlazorOptions = { ...defaultOptions, ...userOptions };
// For development. // For development.
// Simply put a break point here and modify the log level during // Simply put a break point here and modify the log level during
// development to get traces. // development to get traces.
// In the future we will allow for users to configure this. // 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)]; const circuitHandlers: CircuitHandler[] = [new AutoReconnectCircuitHandler(logger)];
window['Blazor'].circuitHandlers = circuitHandlers; window['Blazor'].circuitHandlers = circuitHandlers;
@ -43,8 +51,7 @@ async function boot(options?: BlazorOptions): Promise<void> {
}); });
// pass options.configureSignalR to configure the signalR.HubConnectionBuilder // pass options.configureSignalR to configure the signalR.HubConnectionBuilder
const configureSignalR = (options && options.configureSignalR) || null; const initialConnection = await initializeConnection(options, circuitHandlers, logger);
const initialConnection = await initializeConnection(configureSignalR, circuitHandlers, logger);
const circuits = discoverPrerenderedCircuits(document); const circuits = discoverPrerenderedCircuits(document);
for (let i = 0; i < circuits.length; i++) { 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.'); logger.log(LogLevel.Information, 'No preregistered components to render.');
} }
const reconnect = async (): Promise<boolean> => { const reconnect = async (existingConnection?: signalR.HubConnection): Promise<boolean> => {
if (renderingFailed) { if (renderingFailed) {
// We can't reconnect after a failure, so exit early. // We can't reconnect after a failure, so exit early.
return false; 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))); const results = await Promise.all(circuits.map(circuit => circuit.reconnect(reconnection)));
if (reconnectionFailed(results)) { if (reconnectionFailed(results)) {
@ -82,7 +89,7 @@ async function boot(options?: BlazorOptions): Promise<void> {
window['Blazor'].reconnect = reconnect; window['Blazor'].reconnect = reconnect;
const reconnectTask = reconnect(); const reconnectTask = reconnect(initialConnection);
if (circuit) { if (circuit) {
circuits.push(circuit); circuits.push(circuit);
@ -90,29 +97,29 @@ async function boot(options?: BlazorOptions): Promise<void> {
await reconnectTask; await reconnectTask;
logger.log(LogLevel.Information, 'Blazor server-side application started.');
function reconnectionFailed(results: boolean[]): boolean { function reconnectionFailed(results: boolean[]): boolean {
return !results.reduce((current, next) => current && next, true); 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(); const hubProtocol = new MessagePackHubProtocol();
(hubProtocol as any).name = 'blazorpack'; (hubProtocol as unknown as { name: string }).name = 'blazorpack';
const connectionBuilder = new signalR.HubConnectionBuilder() const connectionBuilder = new signalR.HubConnectionBuilder()
.withUrl('_blazor') .withUrl('_blazor')
.withHubProtocol(hubProtocol); .withHubProtocol(hubProtocol);
if (configureSignalR) { options.configureSignalR(connectionBuilder);
configureSignalR(connectionBuilder);
}
const connection = connectionBuilder.build(); const connection = connectionBuilder.build();
connection.on('JS.BeginInvokeJS', DotNet.jsCallDispatcher.beginInvokeJSFromDotNet); connection.on('JS.BeginInvokeJS', DotNet.jsCallDispatcher.beginInvokeJSFromDotNet);
connection.on('JS.RenderBatch', (browserRendererId: number, batchId: number, batchData: Uint8Array) => { 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); const queue = RenderQueue.getOrCreateQueue(browserRendererId, logger);

View File

@ -28,18 +28,18 @@ export default class RenderQueue {
public processBatch(receivedBatchId: number, batchData: Uint8Array, connection: HubConnection): void { public processBatch(receivedBatchId: number, batchData: Uint8Array, connection: HubConnection): void {
if (receivedBatchId < this.nextBatchId) { 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; return;
} }
if (receivedBatchId > this.nextBatchId) { 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; return;
} }
try { try {
this.nextBatchId++; this.nextBatchId++;
this.logger.log(LogLevel.Information, `Applying batch ${receivedBatchId}.`); this.logger.log(LogLevel.Debug, `Applying batch ${receivedBatchId}.`);
renderBatch(this.browserRendererId, new OutOfProcessRenderBatch(batchData)); renderBatch(this.browserRendererId, new OutOfProcessRenderBatch(batchData));
this.completeBatch(connection, receivedBatchId); this.completeBatch(connection, receivedBatchId);
} catch (error) { } catch (error) {

View File

@ -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("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")); Browser.Equal(string.Empty, () => Browser.FindElement(By.Id("val-set-by-interop")).GetAttribute("value"));
// Once connected, we can
BeginInteractivity(); 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-get-by-interop")).Text);
Browser.Equal("Hello from interop call", () => Browser.FindElement(By.Id("val-set-by-interop")).GetAttribute("value")); Browser.Equal("Hello from interop call", () => Browser.FindElement(By.Id("val-set-by-interop")).GetAttribute("value"));
} }

View File

@ -2,6 +2,8 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure; using Microsoft.AspNetCore.Components.E2ETest.Infrastructure;
@ -26,14 +28,23 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
_serverFixture.BuildWebHostMethod = ComponentsApp.Server.Program.BuildWebHost; _serverFixture.BuildWebHostMethod = ComponentsApp.Server.Program.BuildWebHost;
} }
public DateTime LastLogTimeStamp { get; set; } = DateTime.MinValue;
public override async Task InitializeAsync() public override async Task InitializeAsync()
{ {
await base.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] [Fact]
@ -42,6 +53,18 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
Assert.Equal("Razor Components", Browser.Title); 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] [Fact]
public void HasHeading() public void HasHeading()
{ {

View File

@ -17,4 +17,18 @@
@functions { @functions {
int count; 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;
}
} }

View File

@ -1,4 +1,4 @@
@page @page
@using ComponentsApp.App @using ComponentsApp.App
<!DOCTYPE html> <!DOCTYPE html>
@ -20,7 +20,10 @@
Blazor.start({ Blazor.start({
configureSignalR: function (builder) { configureSignalR: function (builder) {
builder.configureLogging(2); // LogLevel.Information builder.configureLogging(2); // LogLevel.Information
} },
logLevel: 2 // LogLevel.Information
}).then(function () {
window['__aspnetcore__testing__blazor__started__'] = true;
}); });
</script> </script>
</body> </body>

View File

@ -1,6 +1,5 @@
@page @page
@using BasicTestApp.RouterTest @using BasicTestApp.RouterTest
@using Microsoft.AspNetCore.Mvc.ViewFeatures
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
@ -16,7 +15,7 @@
*@ *@
<hr /> <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 src="_framework/components.server.js" autostart="false"></script>
<script> <script>
@ -24,6 +23,12 @@
function setElementValue(element, newValue) { function setElementValue(element, newValue) {
element.value = newValue; element.value = newValue;
return element.value; return element.value;
}
function start() {
Blazor.start({
logLevel: 1 // LogLevel.Debug
});
} }
</script> </script>
</body> </body>

View File

@ -79,7 +79,7 @@ namespace TestServer
subdirApp.UseEndpoints(endpoints => subdirApp.UseEndpoints(endpoints =>
{ {
endpoints.MapFallbackToPage("/PrerenderedHost"); endpoints.MapFallbackToPage("/PrerenderedHost");
endpoints.MapComponentHub<TestRouter>("app"); endpoints.MapComponentHub();
}); });
}); });
} }