[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",
|
// 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 }],
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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"));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,7 @@ namespace TestServer
|
||||||
subdirApp.UseEndpoints(endpoints =>
|
subdirApp.UseEndpoints(endpoints =>
|
||||||
{
|
{
|
||||||
endpoints.MapFallbackToPage("/PrerenderedHost");
|
endpoints.MapFallbackToPage("/PrerenderedHost");
|
||||||
endpoints.MapComponentHub<TestRouter>("app");
|
endpoints.MapComponentHub();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue