diff --git a/SlipItIn/Messages/SyncStateChangedMessage.cs b/SlipItIn/Messages/SyncStateChangedMessage.cs new file mode 100644 index 0000000..d93716b --- /dev/null +++ b/SlipItIn/Messages/SyncStateChangedMessage.cs @@ -0,0 +1,12 @@ +using CommunityToolkit.Mvvm.Messaging.Messages; + +namespace SlipItIn.Messages; + +public sealed record SyncStateInfo(bool IsSyncing, string StatusText, int PendingActions); + +public class SyncStateChangedMessage : ValueChangedMessage +{ + public SyncStateChangedMessage(SyncStateInfo value) : base(value) + { + } +} diff --git a/SlipItIn/Services/GameStateService.cs b/SlipItIn/Services/GameStateService.cs index 51b08f8..8dbfb6c 100644 --- a/SlipItIn/Services/GameStateService.cs +++ b/SlipItIn/Services/GameStateService.cs @@ -12,6 +12,7 @@ public class GameStateService : IGameStateService private readonly ISignalRService _signalR; private readonly ILocalStorageService _localStorage; private readonly IMessenger _messenger; + private readonly SemaphoreSlim _syncLock = new(1, 1); private readonly List _queuedActions = []; @@ -53,16 +54,16 @@ public class GameStateService : IGameStateService _signalR.ConnectionStateChanged += async (_, connected) => { _messenger.Send(new ConnectionStateChangedMessage(connected)); - if (connected) + if (!connected) + return; + + try { - try - { - await ResyncAsync(); - } - catch (Exception ex) - { - _messenger.Send(new ErrorOccurredMessage($"Resync fehlgeschlagen: {ex.Message}")); - } + await ResyncAsync(); + } + catch (Exception ex) + { + _messenger.Send(new ErrorOccurredMessage($"Resync fehlgeschlagen: {ex.Message}")); } }; } @@ -81,14 +82,32 @@ public class GameStateService : IGameStateService if (CurrentPlayerHand is not null) _messenger.Send(new PlayerHandChangedMessage(CurrentPlayerHand)); + + PublishSyncState(false, "Bereit", _queuedActions.Count); } public async Task ResyncAsync() { - if (CurrentGameState?.GameId > 0 && _signalR.IsConnected) - await _signalR.RequestGameStateAsync(CurrentGameState.GameId); + await _syncLock.WaitAsync(); + try + { + PublishSyncState(true, "Syncing...", _queuedActions.Count); - await FlushQueuedActionsAsync(); + if (CurrentGameState?.GameId > 0 && _signalR.IsConnected) + await _signalR.RequestGameStateAsync(CurrentGameState.GameId); + + await FlushQueuedActionsCoreAsync(); + PublishSyncState(false, "Synchronisiert", _queuedActions.Count); + } + catch + { + PublishSyncState(false, "Sync fehlgeschlagen", _queuedActions.Count); + throw; + } + finally + { + _syncLock.Release(); + } } public async Task EnqueueActionAsync(string method, params object[] arguments) @@ -102,21 +121,22 @@ public class GameStateService : IGameStateService _queuedActions.Add(action); await _localStorage.SaveQueuedActionsAsync(_queuedActions); + PublishSyncState(false, "Offline-Aktion gespeichert", _queuedActions.Count); } public async Task FlushQueuedActionsAsync() { - if (!_signalR.IsConnected || _queuedActions.Count == 0) - return; - - var snapshot = _queuedActions.ToList(); - foreach (var action in snapshot) + await _syncLock.WaitAsync(); + try { - await _signalR.SendQueuedActionAsync(action); - _queuedActions.Remove(action); + PublishSyncState(true, "Warteschlange wird gesendet...", _queuedActions.Count); + await FlushQueuedActionsCoreAsync(); + PublishSyncState(false, "Warteschlange synchronisiert", _queuedActions.Count); + } + finally + { + _syncLock.Release(); } - - await _localStorage.SaveQueuedActionsAsync(_queuedActions); } public async Task ClearLocalGameDataAsync() @@ -129,5 +149,36 @@ public class GameStateService : IGameStateService await _localStorage.ClearGameStateAsync(); await _localStorage.ClearPlayerHandAsync(); await _localStorage.ClearQueuedActionsAsync(); + + PublishSyncState(false, "Bereit", 0); + } + + private async Task FlushQueuedActionsCoreAsync() + { + if (!_signalR.IsConnected || _queuedActions.Count == 0) + return; + + var snapshot = _queuedActions.ToList(); + foreach (var action in snapshot) + { + try + { + await _signalR.SendQueuedActionAsync(action); + _queuedActions.Remove(action); + PublishSyncState(true, "Warteschlange wird gesendet...", _queuedActions.Count); + } + catch (Exception ex) + { + _messenger.Send(new ErrorOccurredMessage($"Queue-Aktion fehlgeschlagen ({action.Method}): {ex.Message}")); + break; + } + } + + await _localStorage.SaveQueuedActionsAsync(_queuedActions); + } + + private void PublishSyncState(bool isSyncing, string statusText, int pendingActions) + { + _messenger.Send(new SyncStateChangedMessage(new SyncStateInfo(isSyncing, statusText, pendingActions))); } } diff --git a/SlipItIn/Services/SignalRService.cs b/SlipItIn/Services/SignalRService.cs index b07d87b..9060cca 100644 --- a/SlipItIn/Services/SignalRService.cs +++ b/SlipItIn/Services/SignalRService.cs @@ -38,6 +38,8 @@ public class SignalRService : ISignalRService if (_hubConnection is null) return false; + ConnectionStateChanged?.Invoke(this, false); + try { await _hubConnection.StartAsync(cancellationToken); @@ -46,6 +48,7 @@ public class SignalRService : ISignalRService } catch (Exception ex) { + ConnectionStateChanged?.Invoke(this, false); ErrorReceived?.Invoke(this, $"SignalR-Verbindung fehlgeschlagen: {ex.Message}"); return false; } @@ -56,7 +59,9 @@ public class SignalRService : ISignalRService if (_hubConnection is null) return; - await _hubConnection.StopAsync(); + if (_hubConnection.State is HubConnectionState.Connected or HubConnectionState.Connecting or HubConnectionState.Reconnecting) + await _hubConnection.StopAsync(); + ConnectionStateChanged?.Invoke(this, false); } diff --git a/SlipItIn/ViewModels/GameBoardViewModel.cs b/SlipItIn/ViewModels/GameBoardViewModel.cs index 78737f1..bdd37fe 100644 --- a/SlipItIn/ViewModels/GameBoardViewModel.cs +++ b/SlipItIn/ViewModels/GameBoardViewModel.cs @@ -12,7 +12,9 @@ public partial class GameBoardViewModel : BaseViewModel, IRecipient, IRecipient, IRecipient, - IRecipient + IRecipient, + IRecipient, + IRecipient { private readonly ISignalRService _signalR; private readonly IGameStateService _gameStateService; @@ -33,6 +35,18 @@ public partial class GameBoardViewModel : BaseViewModel, [ObservableProperty] private SlipChallengeDto? pendingChallenge; + [ObservableProperty] + private bool isConnected; + + [ObservableProperty] + private bool isSyncing; + + [ObservableProperty] + private string syncStatusText = "Bereit"; + + [ObservableProperty] + private int pendingSyncActions; + public ObservableCollection MyCards { get; } = []; public ObservableCollection OtherPlayers { get; } = []; @@ -42,6 +56,7 @@ public partial class GameBoardViewModel : BaseViewModel, _gameStateService = gameStateService; _authSession = authSession; + IsConnected = _signalR.IsConnected; IsActive = true; } @@ -164,6 +179,18 @@ public partial class GameBoardViewModel : BaseViewModel, StatusMessage = message.Value; } + public void Receive(SyncStateChangedMessage message) + { + IsSyncing = message.Value.IsSyncing; + SyncStatusText = message.Value.StatusText; + PendingSyncActions = message.Value.PendingActions; + } + + public void Receive(ConnectionStateChangedMessage message) + { + IsConnected = message.Value; + } + private int GetCurrentPlayerId() { var username = _authSession.CurrentUser?.Username; diff --git a/SlipItIn/ViewModels/LobbyViewModel.cs b/SlipItIn/ViewModels/LobbyViewModel.cs index dc17d24..e601597 100644 --- a/SlipItIn/ViewModels/LobbyViewModel.cs +++ b/SlipItIn/ViewModels/LobbyViewModel.cs @@ -13,7 +13,8 @@ public partial class LobbyViewModel : BaseViewModel, IRecipient, IRecipient, IRecipient, - IRecipient + IRecipient, + IRecipient { private readonly ISignalRService _signalR; private readonly IGameStateService _gameStateService; @@ -32,6 +33,15 @@ public partial class LobbyViewModel : BaseViewModel, [ObservableProperty] private bool isConnected; + [ObservableProperty] + private bool isSyncing; + + [ObservableProperty] + private string syncStatusText = "Bereit"; + + [ObservableProperty] + private int pendingSyncActions; + public ObservableCollection Players { get; } = []; public LobbyViewModel(ISignalRService signalR, IGameStateService gameStateService, IAuthSessionService authSession, IApiService apiService) @@ -139,6 +149,13 @@ public partial class LobbyViewModel : BaseViewModel, IsConnected = message.Value; } + public void Receive(SyncStateChangedMessage message) + { + IsSyncing = message.Value.IsSyncing; + SyncStatusText = message.Value.StatusText; + PendingSyncActions = message.Value.PendingActions; + } + private int GetCurrentPlayerId() { var username = _authSession.CurrentUser?.Username; diff --git a/SlipItIn/Views/GameBoardPage.xaml b/SlipItIn/Views/GameBoardPage.xaml index 8ba0222..c27f2bd 100644 --- a/SlipItIn/Views/GameBoardPage.xaml +++ b/SlipItIn/Views/GameBoardPage.xaml @@ -10,6 +10,9 @@