Skip to content

Commit 2046445

Browse files
committed
Some UI
1 parent 88858d0 commit 2046445

14 files changed

+314
-159
lines changed

src/Components/Browser.JS/src/Boot.Server.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@ import { internalFunctions as uriHelperFunctions } from './Services/UriHelper';
77
import { renderBatch } from './Rendering/Renderer';
88
import { fetchBootConfigAsync, loadEmbeddedResourcesAsync } from './BootCommon';
99
import { CircuitHandler } from './Platform/Circuits/CircuitHandler';
10+
import { AutoReconnectCircuitHandler } from './Platform/Circuits/AutoReconnectCircuitHandler';
1011

1112
async function boot() {
12-
const circuitHandlers: CircuitHandler[] = [];
13-
window['Blazor'].addCircuitHandler = (circuitHandler: CircuitHandler) => circuitHandlers.push(circuitHandler);
13+
const circuitHandlers: CircuitHandler[] = [ new AutoReconnectCircuitHandler() ];
14+
window['Blazor'].circuitHandlers = circuitHandlers;
1415

1516
// In the background, start loading the boot config and any embedded resources
1617
const embeddedResourcesPromise = fetchBootConfigAsync().then(bootConfig => {
@@ -30,7 +31,7 @@ async function boot() {
3031
window['Blazor'].reconnect = async () => {
3132
const reconnection = await initializeConnection(circuitHandlers);
3233
if (!await reconnection.invoke<Boolean>('ConnectCircuit', circuitId)) {
33-
throw new Error('Failed to reconnect to the server. The supplied circuitId is no longer valid');
34+
throw new Error('Failed to reconnect to the server. The supplied circuitId is invalid.');
3435
}
3536

3637
circuitHandlers.forEach(h => h.onConnectionUp && h.onConnectionUp());
@@ -61,7 +62,7 @@ async function initializeConnection(circuitHandlers: CircuitHandler[]): Promise<
6162
connection.onclose(error => circuitHandlers.forEach(h => h.onConnectionDown && h.onConnectionDown(error)));
6263
connection.on('JS.Error', error => unhandledError(connection, error));
6364

64-
window['Blazor']._internal.forceCloseConnection = connection.stop;
65+
window['Blazor']._internal.forceCloseConnection = () => connection.stop();
6566

6667
try {
6768
await connection.start();
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { CircuitHandler } from "./CircuitHandler";
2+
export class AutoReconnectCircuitHandler implements CircuitHandler {
3+
modal: HTMLDivElement;
4+
intervalHandle: number | null;
5+
6+
constructor(private maxRetries: number = 3, private retryInterval: number = 3000) {
7+
const modal = document.createElement('div');
8+
modal.className = "modal";
9+
modal.appendChild(document.createTextNode("Attempting to reconnect to the server..."));
10+
document.addEventListener("DOMContentLoaded", function (event) {
11+
document.body.appendChild(modal);
12+
});
13+
14+
this.modal = modal;
15+
this.intervalHandle = null;
16+
}
17+
onConnectionUp() {
18+
this.modal.style.display = 'none';
19+
this.cleanupTimer();
20+
}
21+
async onConnectionDown() {
22+
this.modal.style.display = 'block';
23+
this.cleanupTimer();
24+
25+
let retries = 0;
26+
this.intervalHandle = window.setInterval(async () => {
27+
if (retries++ > this.maxRetries) {
28+
this.cleanupTimer();
29+
}
30+
31+
try {
32+
await window['Blazor'].reconnect();
33+
} catch (err) {
34+
if (retries < this.maxRetries) {
35+
console.error(err);
36+
return;
37+
}
38+
throw err;
39+
}
40+
}, this.retryInterval);
41+
}
42+
43+
cleanupTimer() {
44+
if (this.intervalHandle) {
45+
window.clearTimeout(this.intervalHandle);
46+
}
47+
}
48+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
.modal {
2+
position: fixed;
3+
top: 0;
4+
right: 0;
5+
bottom: 0;
6+
left: 0;
7+
z-index: 1000;
8+
display: none;
9+
overflow: hidden;
10+
background-color: #fff;
11+
opacity: 0.8;
12+
text-align: center;
13+
font-weight: bold;
14+
}

src/Components/Server/src/Circuits/CircuitRegistry.cs

Lines changed: 0 additions & 87 deletions
This file was deleted.
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Diagnostics;
6+
using System.Threading.Tasks;
7+
using Microsoft.Extensions.Caching.Memory;
8+
using Microsoft.Extensions.Logging;
9+
using Microsoft.Extensions.Options;
10+
11+
namespace Microsoft.AspNetCore.Components.Server.Circuits
12+
{
13+
internal class DisconnectedCircuitRegistry
14+
{
15+
private readonly ComponentsServerOptions _options;
16+
private readonly ILogger _logger;
17+
private readonly PostEvictionCallbackRegistration _postEvictionCallback;
18+
19+
public DisconnectedCircuitRegistry(
20+
IOptions<ComponentsServerOptions> options,
21+
ILogger<DisconnectedCircuitRegistry> logger)
22+
{
23+
_options = options.Value;
24+
_logger = logger;
25+
26+
MemoryCache = new MemoryCache(new MemoryCacheOptions
27+
{
28+
SizeLimit = _options.MaxRetainedDisconnectedCircuits,
29+
});
30+
31+
_postEvictionCallback = new PostEvictionCallbackRegistration
32+
{
33+
EvictionCallback = OnEntryEvicted,
34+
};
35+
}
36+
37+
public MemoryCache MemoryCache { get; }
38+
39+
public void AddInactiveCircuit(CircuitHost circuitHost)
40+
{
41+
var entryOptions = new MemoryCacheEntryOptions
42+
{
43+
AbsoluteExpiration = DateTimeOffset.UtcNow.Add(_options.DisconnectedCircuitRetentionPeriod),
44+
Size = 1,
45+
PostEvictionCallbacks = { _postEvictionCallback },
46+
};
47+
48+
MemoryCache.Set(circuitHost.CircuitId, circuitHost, entryOptions);
49+
}
50+
51+
public bool TryGetInactiveCircuit(string circuitId, out CircuitHost host)
52+
{
53+
if (MemoryCache.TryGetValue(circuitId, out host))
54+
{
55+
MemoryCache.Remove(circuitId);
56+
return true;
57+
}
58+
59+
host = null;
60+
return false;
61+
}
62+
63+
private void OnEntryEvicted(object key, object value, EvictionReason reason, object state)
64+
{
65+
switch (reason)
66+
{
67+
case EvictionReason.Expired:
68+
case EvictionReason.Capacity:
69+
// Kick off the dispose in the background, but don't wait for it to finish.
70+
_ = DisposeCircuitHost((CircuitHost)value);
71+
break;
72+
73+
case EvictionReason.Removed:
74+
// The entry was explicitly removed as part of TryGetInactiveCircuit. Nothing to do here.
75+
return;
76+
77+
default:
78+
Debug.Fail($"Unexpected {nameof(EvictionReason)} {reason}");
79+
break;
80+
}
81+
}
82+
83+
private async Task DisposeCircuitHost(CircuitHost value)
84+
{
85+
try
86+
{
87+
await value.DisposeAsync();
88+
}
89+
catch (Exception ex)
90+
{
91+
_logger.UnhandledExceptionDisposingCircuitHost(ex);
92+
}
93+
}
94+
}
95+
}

src/Components/Server/src/ComponentsHub.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ public sealed class ComponentsHub : Hub
1919
{
2020
private static readonly object CircuitKey = new object();
2121
private readonly CircuitFactory _circuitFactory;
22-
private readonly CircuitRegistry _circuitRegistry;
22+
private readonly DisconnectedCircuitRegistry _circuitRegistry;
2323
private readonly ILogger _logger;
2424

2525
/// <summary>
@@ -29,7 +29,7 @@ public sealed class ComponentsHub : Hub
2929
public ComponentsHub(IServiceProvider services, ILogger<ComponentsHub> logger)
3030
{
3131
_circuitFactory = services.GetRequiredService<CircuitFactory>();
32-
_circuitRegistry = services.GetRequiredService<CircuitRegistry>();
32+
_circuitRegistry = services.GetRequiredService<DisconnectedCircuitRegistry>();
3333
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
3434
}
3535

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
6+
namespace Microsoft.AspNetCore.Components.Server
7+
{
8+
/// <summary>
9+
/// Options to configure ASP.NET Core Components.
10+
/// </summary>
11+
public class ComponentsServerOptions
12+
{
13+
/// <summary>
14+
/// Gets or sets a value that determines the maximum number of disconnected circuit state details
15+
/// are retained by the server.
16+
/// <para>
17+
/// When a client disconnects, ASP.NET Core Components attempts to retain state on the server for an
18+
/// interval. This allows the client to re-establish a connection to the existing circuit on the server
19+
/// without losing any state in the event of transient connection issues.
20+
/// </para>
21+
/// <para>
22+
/// This value determines the maximium number of circuit states retained by the server.
23+
/// <seealso cref="DisconnectedCircuitRetentionPeriod"/>
24+
/// </para>
25+
/// </summary>
26+
/// <value>
27+
/// Defaults to <c>100</c>.
28+
/// </value>
29+
public int MaxRetainedDisconnectedCircuits { get; set; } = 100;
30+
31+
/// <summary>
32+
/// Gets or sets a value that determines the maximum duration state for a disconnected circuit is
33+
/// retained on the server.
34+
/// <para>
35+
/// When a client disconnects, ASP.NET Core Components attempts to retain state on the server for an
36+
/// interval. This allows the client to re-establish a connection to the existing circuit on the server
37+
/// without losing any state in the event of transient connection issues.
38+
/// </para>
39+
/// <para>
40+
/// This value determines the maximium duration circuit state is retained by the server before being evicted.
41+
/// <seealso cref="MaxRetainedDisconnectedCircuits"/>
42+
/// </para>
43+
/// </summary>
44+
/// <value>
45+
/// Defaults to <c>3 minutes</c>.
46+
/// </value>
47+
public TimeSpan DisconnectedCircuitRetentionPeriod { get; set; } = TimeSpan.FromMinutes(3);
48+
}
49+
}

src/Components/Server/src/DependencyInjection/RazorComponentsServiceCollectionExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ private static void AddStandardRazorComponentsServices(IServiceCollection servic
102102
services.TryAddScoped<IJSRuntimeAccessor, DefaultJSRuntimeAccessor>();
103103
services.TryAddScoped(s => s.GetRequiredService<IJSRuntimeAccessor>().JSRuntime);
104104
services.TryAddScoped<IUriHelper, RemoteUriHelper>();
105-
services.TryAddSingleton<CircuitRegistry>();
105+
services.TryAddSingleton<DisconnectedCircuitRegistry>();
106106

107107
// We've discussed with the SignalR team and believe it's OK to have repeated
108108
// calls to AddSignalR (making the nonfirst ones no-ops). If we want to change

src/Components/Server/src/LoggerExtensions.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,19 @@ namespace Microsoft.AspNetCore.Components.Server
99
internal static class LoggerExtensions
1010
{
1111
private static readonly Action<ILogger, string, Exception> _unhandledExceptionRenderingComponent;
12+
private static readonly Action<ILogger, string, Exception> _unhandledExceptionDisposingCircuitHost;
1213

1314
static LoggerExtensions()
1415
{
1516
_unhandledExceptionRenderingComponent = LoggerMessage.Define<string>(
1617
LogLevel.Warning,
1718
new EventId(1, "ExceptionRenderingComponent"),
1819
"Unhandled exception rendering component: {Message}");
20+
21+
_unhandledExceptionDisposingCircuitHost = LoggerMessage.Define<string>(
22+
LogLevel.Warning,
23+
new EventId(2, "ExceptionInvokingCircuitHandler"),
24+
"Unhandled exception disposing circuit host: {Message}");
1925
}
2026

2127
public static void UnhandledExceptionRenderingComponent(this ILogger logger, Exception exception)
@@ -25,5 +31,13 @@ public static void UnhandledExceptionRenderingComponent(this ILogger logger, Exc
2531
exception.Message,
2632
exception);
2733
}
34+
35+
public static void UnhandledExceptionDisposingCircuitHost(this ILogger logger, Exception exception)
36+
{
37+
_unhandledExceptionDisposingCircuitHost(
38+
logger,
39+
exception.Message,
40+
exception);
41+
}
2842
}
2943
}

0 commit comments

Comments
 (0)