Synchronise les parties entre les deux devices
This commit is contained in:
89
ChessCubing.Server/Social/CollaborativeMatchCoordinator.cs
Normal file
89
ChessCubing.Server/Social/CollaborativeMatchCoordinator.cs
Normal file
@@ -0,0 +1,89 @@
|
||||
namespace ChessCubing.Server.Social;
|
||||
|
||||
public sealed class CollaborativeMatchCoordinator
|
||||
{
|
||||
private readonly object _sync = new();
|
||||
private readonly Dictionary<string, CollaborativeSessionState> _sessions = new(StringComparer.Ordinal);
|
||||
|
||||
public void RegisterSession(PlaySessionResponse session)
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
_sessions[session.SessionId] = new CollaborativeSessionState(
|
||||
session.SessionId,
|
||||
new HashSet<string>(StringComparer.Ordinal)
|
||||
{
|
||||
session.WhiteSubject,
|
||||
session.BlackSubject,
|
||||
session.InitiatorSubject,
|
||||
session.RecipientSubject,
|
||||
},
|
||||
null,
|
||||
0);
|
||||
}
|
||||
}
|
||||
|
||||
public bool CanAccessSession(string sessionId, string subject)
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
return _sessions.TryGetValue(sessionId, out var session)
|
||||
&& session.ParticipantSubjects.Contains(subject);
|
||||
}
|
||||
}
|
||||
|
||||
public CollaborativeMatchStateMessage? GetLatestState(string sessionId, string subject)
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
if (!_sessions.TryGetValue(sessionId, out var session) ||
|
||||
!session.ParticipantSubjects.Contains(subject))
|
||||
{
|
||||
throw new SocialValidationException("Cette session de partie est introuvable ou n'est pas accessible.");
|
||||
}
|
||||
|
||||
return session.LatestState;
|
||||
}
|
||||
}
|
||||
|
||||
public CollaborativeMatchStateMessage PublishState(
|
||||
string sessionId,
|
||||
string subject,
|
||||
string? matchJson,
|
||||
string route)
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
if (!_sessions.TryGetValue(sessionId, out var session) ||
|
||||
!session.ParticipantSubjects.Contains(subject))
|
||||
{
|
||||
throw new SocialValidationException("Cette session de partie est introuvable ou n'est pas accessible.");
|
||||
}
|
||||
|
||||
var revision = session.Revision + 1;
|
||||
var message = new CollaborativeMatchStateMessage
|
||||
{
|
||||
SessionId = sessionId,
|
||||
MatchJson = matchJson,
|
||||
Route = string.IsNullOrWhiteSpace(route) ? "/application.html" : route.Trim(),
|
||||
SenderSubject = subject,
|
||||
Revision = revision,
|
||||
UpdatedUtc = DateTime.UtcNow,
|
||||
};
|
||||
|
||||
_sessions[sessionId] = session with
|
||||
{
|
||||
Revision = revision,
|
||||
LatestState = message,
|
||||
};
|
||||
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed record CollaborativeSessionState(
|
||||
string SessionId,
|
||||
HashSet<string> ParticipantSubjects,
|
||||
CollaborativeMatchStateMessage? LatestState,
|
||||
long Revision);
|
||||
}
|
||||
@@ -134,4 +134,19 @@ public sealed class PlaySessionResponse
|
||||
public DateTime ConfirmedUtc { get; init; }
|
||||
}
|
||||
|
||||
public sealed class CollaborativeMatchStateMessage
|
||||
{
|
||||
public string SessionId { get; init; } = string.Empty;
|
||||
|
||||
public string? MatchJson { get; init; }
|
||||
|
||||
public string Route { get; init; } = "/application.html";
|
||||
|
||||
public string SenderSubject { get; init; } = string.Empty;
|
||||
|
||||
public long Revision { get; init; }
|
||||
|
||||
public DateTime UpdatedUtc { get; init; }
|
||||
}
|
||||
|
||||
public sealed class SocialValidationException(string message) : Exception(message);
|
||||
|
||||
@@ -9,12 +9,14 @@ public sealed class SocialHub(
|
||||
ConnectedUserTracker tracker,
|
||||
MySqlSocialStore socialStore,
|
||||
MySqlUserProfileStore profileStore,
|
||||
PlayInviteCoordinator playInviteCoordinator) : Hub
|
||||
PlayInviteCoordinator playInviteCoordinator,
|
||||
CollaborativeMatchCoordinator collaborativeMatchCoordinator) : Hub
|
||||
{
|
||||
private readonly ConnectedUserTracker _tracker = tracker;
|
||||
private readonly MySqlSocialStore _socialStore = socialStore;
|
||||
private readonly MySqlUserProfileStore _profileStore = profileStore;
|
||||
private readonly PlayInviteCoordinator _playInviteCoordinator = playInviteCoordinator;
|
||||
private readonly CollaborativeMatchCoordinator _collaborativeMatchCoordinator = collaborativeMatchCoordinator;
|
||||
|
||||
public override async Task OnConnectedAsync()
|
||||
{
|
||||
@@ -99,6 +101,7 @@ public sealed class SocialHub(
|
||||
if (accept)
|
||||
{
|
||||
var accepted = _playInviteCoordinator.AcceptInvite(inviteId, recipient.Subject);
|
||||
_collaborativeMatchCoordinator.RegisterSession(accepted.Session);
|
||||
await Clients.Users([accepted.SenderSubject, accepted.RecipientSubject])
|
||||
.SendAsync("PlayInviteAccepted", accepted.Session, Context.ConnectionAborted);
|
||||
return;
|
||||
@@ -130,6 +133,50 @@ public sealed class SocialHub(
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<CollaborativeMatchStateMessage?> JoinPlaySession(string sessionId)
|
||||
{
|
||||
var siteUser = RequireCurrentUser();
|
||||
var normalizedSessionId = sessionId.Trim();
|
||||
|
||||
try
|
||||
{
|
||||
var latestState = _collaborativeMatchCoordinator.GetLatestState(normalizedSessionId, siteUser.Subject);
|
||||
await Groups.AddToGroupAsync(Context.ConnectionId, BuildSessionGroupName(normalizedSessionId), Context.ConnectionAborted);
|
||||
return latestState;
|
||||
}
|
||||
catch (SocialValidationException exception)
|
||||
{
|
||||
throw new HubException(exception.Message);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task LeavePlaySession(string sessionId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(sessionId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await Groups.RemoveFromGroupAsync(Context.ConnectionId, BuildSessionGroupName(sessionId.Trim()), Context.ConnectionAborted);
|
||||
}
|
||||
|
||||
public async Task PublishMatchState(string sessionId, string? matchJson, string route)
|
||||
{
|
||||
var siteUser = RequireCurrentUser();
|
||||
var normalizedSessionId = sessionId.Trim();
|
||||
|
||||
try
|
||||
{
|
||||
var message = _collaborativeMatchCoordinator.PublishState(normalizedSessionId, siteUser.Subject, matchJson, route);
|
||||
await Clients.GroupExcept(BuildSessionGroupName(normalizedSessionId), [Context.ConnectionId])
|
||||
.SendAsync("CollaborativeMatchStateUpdated", message, Context.ConnectionAborted);
|
||||
}
|
||||
catch (SocialValidationException exception)
|
||||
{
|
||||
throw new HubException(exception.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private AuthenticatedSiteUser RequireCurrentUser()
|
||||
=> AuthenticatedSiteUserFactory.FromClaimsPrincipal(Context.User ?? new System.Security.Claims.ClaimsPrincipal())
|
||||
?? throw new HubException("La session utilisateur est incomplete.");
|
||||
@@ -165,4 +212,7 @@ public sealed class SocialHub(
|
||||
IsOnline = isOnline,
|
||||
});
|
||||
}
|
||||
|
||||
private static string BuildSessionGroupName(string sessionId)
|
||||
=> $"play-session:{sessionId}";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user