Skip to content

Commit

Permalink
feat: improve RpcPeer connection handling, + RpcLimits, add WebSocket…
Browse files Browse the repository at this point in the history
…Channel.MaxItemSize, improve RpcPeerStateMonitor, etc.
  • Loading branch information
alexyakunin committed Dec 3, 2023
1 parent f89eb3b commit c153b97
Show file tree
Hide file tree
Showing 21 changed files with 402 additions and 141 deletions.
1 change: 1 addition & 0 deletions samples/TodoApp/UI/Pages/TodoPage.razor
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

<h1>Todo List</h1>

<TextConnectionStatus />
<StateOfStateBadge State="@State" />
<Div Margin="Margin.Is1.OnY">
Updated: <b><MomentsAgoBadge Value="LastStateUpdateTime" /></b>
Expand Down
62 changes: 19 additions & 43 deletions samples/TodoApp/UI/Shared/BarConnectionStatus.razor
Original file line number Diff line number Diff line change
@@ -1,22 +1,15 @@
@using Stl.Rpc
@inherits ComputedStateComponent<RpcPeerState?>
@inject RpcClientPeerReconnectDelayer RpcClientPeerReconnectDelayer
@inherits StatefulComponentBase<IState<RpcPeerComputedState>>

@{
var m = State.ValueOrDefault ?? new RpcPeerState(true);
var isReconnecting = !m.IsConnected
&& m.ReconnectsAt <= RpcClientPeerReconnectDelayer.Clock.Now;
var message = m.IsConnected
? "Connected."
: m.Error?.Message.Trim() ?? "Unknown error.";
if (!message.EndsWith(".") && !message.EndsWith("!") && !message.EndsWith("?"))
message += ".";
var iconName = m.IsConnected == false
? FontAwesomeIcons.ExclamationTriangle
: FontAwesomeIcons.Cloud;
var textColor = m.IsConnected == false
? TextColor.Warning
: TextColor.Default;
var m = State.Value;
var isConnected = m.IsOrLikelyConnected;
var iconName = isConnected
? FontAwesomeIcons.Cloud
: FontAwesomeIcons.ExclamationTriangle;
var textColor = isConnected
? TextColor.Default
: TextColor.Warning;
}

<BarItem>
Expand All @@ -26,42 +19,25 @@
</BarDropdownToggle>
<BarDropdownMenu>
<BarDropdownItem TextColor="@textColor">
<span>@message</span>
@if (!m.IsConnected) {
if (isReconnecting) {
<span> Reconnecting... </span>
}
else {
<span> Will reconnect <TimerBadge ExpiresAt="m.ReconnectsAt"/>. </span>
<Button Color="Color.Success" Clicked="@TryReconnect">Reconnect</Button>
}
<span>@m.GetActivityDescription(true)</span>
@if (m.ReconnectsIn is { } reconnectsIn) {
<span> Will reconnect <TimerBadge ExpiresAt="@(Clock.Now + reconnectsIn)"/>. </span>
<Button Color="Color.Success" Clicked="@Reconnect">Reconnect</Button>
}
</BarDropdownItem>
</BarDropdownMenu>
</BarDropdown>
</BarItem>

@code {
private RpcPeerStateMonitor? _monitor;
[Inject] private RpcPeerStateMonitor Monitor { get; init; } = null!;
[Inject] private IMomentClock Clock { get; init; } = null!;

[Parameter]
public string CssClass { get; set; } = "";
[Parameter] public string CssClass { get; set; } = "";

protected override void OnInitialized()
{
_monitor = Services.GetService<RpcPeerStateMonitor>();
_monitor?.Start();
base.OnInitialized();
}
protected override IState<RpcPeerComputedState> CreateState()
=> Monitor.ComputedState;

protected override async Task<RpcPeerState?> ComputeState(CancellationToken cancellationToken)
{
if (_monitor == null)
return null;

return await _monitor.State.Use(cancellationToken).ConfigureAwait(false);
}

private void TryReconnect()
private void Reconnect()
=> Services.RpcHub().InternalServices.ClientPeerReconnectDelayer.CancelDelays();
}
32 changes: 32 additions & 0 deletions samples/TodoApp/UI/Shared/TextConnectionStatus.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
@using Stl.Rpc
@inherits StatefulComponentBase<IState<RpcPeerComputedState>>

@{
var m = State.Value;
var isConnected = m.IsOrLikelyConnected;
}

<Div Margin="Margin.Is1.OnY" TextColor="@(isConnected ? TextColor.Default : TextColor.Warning)">
<span>Connection state: </span>
<strong>
<span>@(m.GetActivityDescription(true))</span>
@if (m.ReconnectsIn is { } reconnectsIn) {
<span> Will reconnect <TimerBadge ExpiresAt="@(Clock.Now + reconnectsIn)"/>. </span>
<Anchor TextColor="@TextColor.Success" Clicked="@Reconnect">Reconnect</Anchor>
}
</strong>
</Div>

@code {
[Inject] private RpcPeerStateMonitor Monitor { get; init; } = null!;
[Inject] private IMomentClock Clock { get; init; } = null!;

[Parameter]
public string CssClass { get; set; } = "";

protected override IState<RpcPeerComputedState> CreateState()
=> Monitor.ComputedState;

private void Reconnect()
=> Services.RpcHub().InternalServices.ClientPeerReconnectDelayer.CancelDelays();
}
2 changes: 1 addition & 1 deletion samples/TodoApp/UI/StartupHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ public static void ConfigureServices(IServiceCollection services, WebAssemblyHos
// Fusion services
var fusion = services.AddFusion();
fusion.AddAuthClient();
fusion.AddRpcPeerStateMonitor();
fusion.AddBlazor().AddAuthentication().AddPresenceReporter();

var rpc = fusion.Rpc;
Expand Down Expand Up @@ -71,6 +70,7 @@ public static void ConfigureSharedServices(IServiceCollection services)
fusion.AddComputedGraphPruner(_ => new() { CheckPeriod = TimeSpan.FromSeconds(10) });
fusion.AddFusionTime();
fusion.AddService<TodoUI>(ServiceLifetime.Scoped);
services.AddScoped(c => new RpcPeerStateMonitor(c, OSInfo.IsAnyClient ? RpcPeerRef.Default : null));

// Default update delay is 0.5s
services.AddScoped<IUpdateDelayer>(c => new UpdateDelayer(c.UIActionTracker(), 0.5));
Expand Down
14 changes: 0 additions & 14 deletions src/Stl.Fusion/Extensions/FusionBuilderExt.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using Stl.Fusion.Extensions.Internal;
using Stl.Rpc;

namespace Stl.Fusion.Extensions;

Expand All @@ -14,17 +13,4 @@ public static FusionBuilder AddFusionTime(this FusionBuilder fusion,
fusion.AddService<IFusionTime, FusionTime>();
return fusion;
}

public static FusionBuilder AddRpcPeerStateMonitor(this FusionBuilder fusion,
Func<IServiceProvider, RpcPeerRef>? peerRefResolver = null)
{
var services = fusion.Services;
services.AddSingleton(c => {
var monitor = new RpcPeerStateMonitor(c);
if (peerRefResolver != null)
monitor.PeerRef = peerRefResolver.Invoke(c);
return monitor;
});
return fusion;
}
}
41 changes: 41 additions & 0 deletions src/Stl.Fusion/Extensions/RpcPeerComputedState.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
namespace Stl.Fusion.Extensions;

public enum RpcPeerComputedStateKind
{
Connected = 0,
JustDisconnected,
Disconnected,
Reconnecting,
}

public sealed record RpcPeerComputedState(
RpcPeerComputedStateKind Kind,
Exception? LastError = null,
TimeSpan? ReconnectsIn = null)
{
public bool IsConnected => Kind == RpcPeerComputedStateKind.Connected;
public bool IsOrLikelyConnected =>
Kind == RpcPeerComputedStateKind.Connected
|| Kind == RpcPeerComputedStateKind.JustDisconnected;

public string GetActivityDescription(bool useLastError = false)
{
switch (Kind) {
case RpcPeerComputedStateKind.Connected:
return "Connected.";
case RpcPeerComputedStateKind.JustDisconnected:
return "Connected, checking...";
case RpcPeerComputedStateKind.Reconnecting:
return "Reconnecting...";
}
if (LastError == null || !useLastError)
return "Disconnected.";

var message = LastError.Message.Trim();
if (!(message.EndsWith(".", StringComparison.Ordinal)
|| message.EndsWith("!", StringComparison.Ordinal)
|| message.EndsWith("?", StringComparison.Ordinal)))
message += ".";
return "Disconnected: " + message;
}
}
44 changes: 40 additions & 4 deletions src/Stl.Fusion/Extensions/RpcPeerState.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,42 @@
using Stl.Rpc.Infrastructure;

namespace Stl.Fusion.Extensions;

public record RpcPeerState(
bool IsConnected,
Exception? Error = null,
Moment ReconnectsAt = default); // Relative to CpuClock.Now
// Any Moment below is derived with RpcHub.Clock, which is CpuClock

public abstract record RpcPeerState
{
public abstract Moment EnteredAt { get; }

public RpcPeerConnectedState ToConnected(Moment now)
=> this as RpcPeerConnectedState ?? new RpcPeerConnectedState(now);

public RpcPeerDisconnectedState ToDisconnected(Moment now, Moment reconnectsAt, RpcPeerConnectionState state)
=> ToDisconnected(now, reconnectsAt, state.Error);
public RpcPeerDisconnectedState ToDisconnected(Moment now, Moment reconnectsAt, Exception? lastError)
{
if (this is not RpcPeerDisconnectedState d)
return new RpcPeerDisconnectedState(now, reconnectsAt, lastError);

if (reconnectsAt == d.ReconnectsAt && d.LastError == lastError)
return d;

return new RpcPeerDisconnectedState(d.DisconnectedAt, reconnectsAt, lastError ?? d.LastError);
}
}

public sealed record RpcPeerConnectedState(
Moment ConnectedAt
) : RpcPeerState
{
public override Moment EnteredAt => ConnectedAt;
}

public sealed record RpcPeerDisconnectedState(
Moment DisconnectedAt,
Moment ReconnectsAt, // < Now = tries to reconnect now
Exception? LastError
) : RpcPeerState
{
public override Moment EnteredAt => DisconnectedAt;
}
Loading

0 comments on commit c153b97

Please sign in to comment.