Synchronise les parties entre les deux devices
This commit is contained in:
@@ -1,4 +1,7 @@
|
||||
using System.Security.Claims;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using ChessCubing.App.Models;
|
||||
using ChessCubing.App.Models.Social;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
@@ -10,17 +13,24 @@ public sealed class SocialRealtimeService(
|
||||
NavigationManager navigation,
|
||||
AuthenticationStateProvider authenticationStateProvider) : IAsyncDisposable
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
};
|
||||
|
||||
private readonly NavigationManager _navigation = navigation;
|
||||
private readonly AuthenticationStateProvider _authenticationStateProvider = authenticationStateProvider;
|
||||
private readonly SemaphoreSlim _gate = new(1, 1);
|
||||
|
||||
private HubConnection? _hubConnection;
|
||||
private string? _currentSubject;
|
||||
private string? _joinedPlaySessionId;
|
||||
private HashSet<string> _knownPresenceSubjects = new(StringComparer.Ordinal);
|
||||
private HashSet<string> _onlineSubjects = new(StringComparer.Ordinal);
|
||||
private PlayInviteMessage? _incomingPlayInvite;
|
||||
private PlayInviteMessage? _outgoingPlayInvite;
|
||||
private PlaySessionResponse? _activePlaySession;
|
||||
private CollaborativeMatchSnapshot? _collaborativeSnapshot;
|
||||
private string? _lastInviteNotice;
|
||||
private int _socialVersion;
|
||||
private bool _started;
|
||||
@@ -31,6 +41,10 @@ public sealed class SocialRealtimeService(
|
||||
|
||||
public PlayInviteMessage? OutgoingPlayInvite => _outgoingPlayInvite;
|
||||
|
||||
public PlaySessionResponse? ActivePlaySession => _activePlaySession;
|
||||
|
||||
public CollaborativeMatchSnapshot? CollaborativeSnapshot => _collaborativeSnapshot;
|
||||
|
||||
public string? LastInviteNotice => _lastInviteNotice;
|
||||
|
||||
public int SocialVersion => _socialVersion;
|
||||
@@ -63,6 +77,43 @@ public sealed class SocialRealtimeService(
|
||||
: null;
|
||||
}
|
||||
|
||||
public async Task EnsureJoinedPlaySessionAsync(string? sessionId)
|
||||
{
|
||||
var normalizedSessionId = sessionId?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(normalizedSessionId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await EnsureStartedAsync();
|
||||
await JoinPlaySessionCoreAsync(normalizedSessionId);
|
||||
}
|
||||
|
||||
public async Task PublishMatchStateAsync(MatchState? match, string route)
|
||||
{
|
||||
var sessionId = match?.CollaborationSessionId
|
||||
?? _activePlaySession?.SessionId
|
||||
?? _joinedPlaySessionId;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(sessionId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await EnsureJoinedPlaySessionAsync(sessionId);
|
||||
|
||||
if (_hubConnection is null || _hubConnection.State != HubConnectionState.Connected)
|
||||
{
|
||||
throw new InvalidOperationException("La connexion temps reel n'est pas prete.");
|
||||
}
|
||||
|
||||
var payload = match is null
|
||||
? null
|
||||
: JsonSerializer.Serialize(match, JsonOptions);
|
||||
|
||||
await _hubConnection.InvokeAsync("PublishMatchState", sessionId, payload, route);
|
||||
}
|
||||
|
||||
public async Task SendPlayInviteAsync(string recipientSubject, string recipientColor)
|
||||
{
|
||||
_lastInviteNotice = null;
|
||||
@@ -105,13 +156,6 @@ public sealed class SocialRealtimeService(
|
||||
await _hubConnection.InvokeAsync("CancelPlayInvite", inviteId);
|
||||
}
|
||||
|
||||
public PlaySessionResponse? TakeAcceptedPlaySession()
|
||||
{
|
||||
var session = _activePlaySession;
|
||||
_activePlaySession = null;
|
||||
return session;
|
||||
}
|
||||
|
||||
public void ClearInviteNotice()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_lastInviteNotice))
|
||||
@@ -154,10 +198,11 @@ public sealed class SocialRealtimeService(
|
||||
var authState = await _authenticationStateProvider.GetAuthenticationStateAsync();
|
||||
var user = authState.User;
|
||||
var subject = ResolveSubject(user);
|
||||
var preserveSessionState = string.Equals(subject, _currentSubject, StringComparison.Ordinal);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(subject))
|
||||
{
|
||||
await DisconnectUnsafeAsync();
|
||||
await DisconnectUnsafeAsync(clearSessionState: true);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -168,12 +213,17 @@ public sealed class SocialRealtimeService(
|
||||
return;
|
||||
}
|
||||
|
||||
await DisconnectUnsafeAsync();
|
||||
await DisconnectUnsafeAsync(clearSessionState: !preserveSessionState);
|
||||
|
||||
_currentSubject = subject;
|
||||
_hubConnection = BuildHubConnection();
|
||||
RegisterHandlers(_hubConnection);
|
||||
await _hubConnection.StartAsync();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(_joinedPlaySessionId))
|
||||
{
|
||||
await JoinPlaySessionCoreAsync(_joinedPlaySessionId);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -241,18 +291,31 @@ public sealed class SocialRealtimeService(
|
||||
RaiseChanged();
|
||||
});
|
||||
|
||||
connection.On<PlaySessionResponse>("PlayInviteAccepted", session =>
|
||||
connection.On<CollaborativeMatchStateMessage>("CollaborativeMatchStateUpdated", message =>
|
||||
{
|
||||
ApplyCollaborativeState(message);
|
||||
RaiseChanged();
|
||||
});
|
||||
|
||||
connection.On<PlaySessionResponse>("PlayInviteAccepted", async session =>
|
||||
{
|
||||
_incomingPlayInvite = null;
|
||||
_outgoingPlayInvite = null;
|
||||
_activePlaySession = session;
|
||||
_lastInviteNotice = "La partie est confirmee. Les noms ont ete pre-remplis dans l'application.";
|
||||
_lastInviteNotice = "La partie est confirmee. Les deux ecrans resteront synchronises pendant le match.";
|
||||
await JoinPlaySessionCoreAsync(session.SessionId);
|
||||
RaiseChanged();
|
||||
});
|
||||
|
||||
connection.Reconnected += async _ =>
|
||||
{
|
||||
await RequestPresenceSnapshotAsync();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(_joinedPlaySessionId))
|
||||
{
|
||||
await JoinPlaySessionCoreAsync(_joinedPlaySessionId);
|
||||
}
|
||||
|
||||
RaiseChanged();
|
||||
};
|
||||
|
||||
@@ -282,6 +345,39 @@ public sealed class SocialRealtimeService(
|
||||
}
|
||||
}
|
||||
|
||||
private async Task JoinPlaySessionCoreAsync(string sessionId)
|
||||
{
|
||||
if (_hubConnection is null || _hubConnection.State != HubConnectionState.Connected)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(_joinedPlaySessionId) &&
|
||||
!string.Equals(_joinedPlaySessionId, sessionId, StringComparison.Ordinal))
|
||||
{
|
||||
try
|
||||
{
|
||||
await _hubConnection.InvokeAsync("LeavePlaySession", _joinedPlaySessionId);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// La prochaine connexion recreera les groupes si besoin.
|
||||
}
|
||||
}
|
||||
|
||||
var latestState = await _hubConnection.InvokeAsync<CollaborativeMatchStateMessage?>("JoinPlaySession", sessionId);
|
||||
_joinedPlaySessionId = sessionId;
|
||||
|
||||
if (latestState is not null)
|
||||
{
|
||||
ApplyCollaborativeState(latestState);
|
||||
}
|
||||
else if (_collaborativeSnapshot?.SessionId != sessionId)
|
||||
{
|
||||
_collaborativeSnapshot = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyInvite(PlayInviteMessage invite)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_currentSubject))
|
||||
@@ -300,15 +396,59 @@ public sealed class SocialRealtimeService(
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DisconnectUnsafeAsync()
|
||||
private void ApplyCollaborativeState(CollaborativeMatchStateMessage message)
|
||||
{
|
||||
_currentSubject = null;
|
||||
if (_collaborativeSnapshot is not null &&
|
||||
string.Equals(_collaborativeSnapshot.SessionId, message.SessionId, StringComparison.Ordinal) &&
|
||||
message.Revision <= _collaborativeSnapshot.Revision)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
MatchState? match = null;
|
||||
if (!string.IsNullOrWhiteSpace(message.MatchJson))
|
||||
{
|
||||
try
|
||||
{
|
||||
match = JsonSerializer.Deserialize<MatchState>(message.MatchJson, JsonOptions);
|
||||
if (match is not null)
|
||||
{
|
||||
match.CollaborationSessionId = message.SessionId;
|
||||
MatchEngine.NormalizeRecoveredMatch(match);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
match = null;
|
||||
}
|
||||
}
|
||||
|
||||
_collaborativeSnapshot = new CollaborativeMatchSnapshot
|
||||
{
|
||||
SessionId = message.SessionId,
|
||||
Match = match,
|
||||
Route = string.IsNullOrWhiteSpace(message.Route) ? "/application.html" : message.Route,
|
||||
SenderSubject = message.SenderSubject,
|
||||
Revision = message.Revision,
|
||||
UpdatedUtc = message.UpdatedUtc,
|
||||
};
|
||||
}
|
||||
|
||||
private async Task DisconnectUnsafeAsync(bool clearSessionState)
|
||||
{
|
||||
_currentSubject = clearSessionState ? null : _currentSubject;
|
||||
_knownPresenceSubjects.Clear();
|
||||
_onlineSubjects.Clear();
|
||||
_incomingPlayInvite = null;
|
||||
_outgoingPlayInvite = null;
|
||||
_activePlaySession = null;
|
||||
_lastInviteNotice = null;
|
||||
|
||||
if (clearSessionState)
|
||||
{
|
||||
_activePlaySession = null;
|
||||
_joinedPlaySessionId = null;
|
||||
_collaborativeSnapshot = null;
|
||||
_lastInviteNotice = null;
|
||||
}
|
||||
|
||||
if (_hubConnection is not null)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user