Updated synchronization progress implementation.
This commit is contained in:
@@ -23,7 +23,7 @@ namespace Wino.Core.Services;
|
||||
/// Singleton manager that handles synchronizer instances and operations for all accounts.
|
||||
/// Replaces the old WinoServerConnectionManager functionality.
|
||||
/// </summary>
|
||||
public class SynchronizationManager : ISynchronizationManager
|
||||
public class SynchronizationManager : ISynchronizationManager, IRecipient<AccountSynchronizerStateChanged>
|
||||
{
|
||||
private static readonly Lazy<SynchronizationManager> _instance = new(() => new SynchronizationManager());
|
||||
public static SynchronizationManager Instance => _instance.Value;
|
||||
@@ -31,6 +31,8 @@ public class SynchronizationManager : ISynchronizationManager
|
||||
private readonly ConcurrentDictionary<Guid, IWinoSynchronizerBase> _synchronizerCache = new();
|
||||
private readonly ConcurrentDictionary<Guid, CancellationTokenSource> _accountSynchronizationCancellationSources = new();
|
||||
private readonly ConcurrentDictionary<Guid, SemaphoreSlim> _calendarSynchronizationLocks = new();
|
||||
private readonly ConcurrentDictionary<Guid, AccountSynchronizationProgress> _mailSynchronizationProgress = new();
|
||||
private readonly ConcurrentDictionary<Guid, AccountSynchronizationProgress> _calendarSynchronizationProgress = new();
|
||||
private readonly SemaphoreSlim _initializationSemaphore = new(1, 1);
|
||||
private readonly ILogger _logger = Log.ForContext<SynchronizationManager>();
|
||||
|
||||
@@ -41,6 +43,7 @@ public class SynchronizationManager : ISynchronizationManager
|
||||
private INotificationBuilder _notificationBuilder;
|
||||
|
||||
private bool _isInitialized = false;
|
||||
private bool _isRegisteredForProgressMessages;
|
||||
|
||||
private SynchronizationManager() { }
|
||||
|
||||
@@ -73,6 +76,11 @@ public class SynchronizationManager : ISynchronizationManager
|
||||
|
||||
// DO NOT create synchronizers here to avoid requiring window handles during initialization.
|
||||
// Synchronizers will be created lazily when first accessed via GetOrCreateSynchronizerAsync.
|
||||
if (!_isRegisteredForProgressMessages)
|
||||
{
|
||||
WeakReferenceMessenger.Default.Register<AccountSynchronizerStateChanged>(this);
|
||||
_isRegisteredForProgressMessages = true;
|
||||
}
|
||||
|
||||
_isInitialized = true;
|
||||
_logger.Information("SynchronizationManager dependencies initialized. Synchronizers will be created lazily.");
|
||||
@@ -219,6 +227,21 @@ public class SynchronizationManager : ISynchronizationManager
|
||||
return false;
|
||||
}
|
||||
|
||||
public AccountSynchronizationProgress GetSynchronizationProgress(Guid accountId, SynchronizationProgressCategory category)
|
||||
{
|
||||
EnsureInitialized();
|
||||
|
||||
return category switch
|
||||
{
|
||||
SynchronizationProgressCategory.Calendar => _calendarSynchronizationProgress.TryGetValue(accountId, out var calendarProgress)
|
||||
? calendarProgress
|
||||
: AccountSynchronizationProgress.Idle(accountId, SynchronizationProgressCategory.Calendar),
|
||||
_ => _mailSynchronizationProgress.TryGetValue(accountId, out var mailProgress)
|
||||
? mailProgress
|
||||
: AccountSynchronizationProgress.Idle(accountId, SynchronizationProgressCategory.Mail)
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Queues a request to the corresponding account's synchronizer with optional synchronization triggering.
|
||||
/// Automatically determines whether to trigger mail or calendar synchronization based on the request type.
|
||||
@@ -651,6 +674,9 @@ public class SynchronizationManager : ISynchronizationManager
|
||||
_logger.Information("Canceled ongoing synchronizations for account {AccountId}", accountId);
|
||||
}
|
||||
|
||||
PublishSynchronizationProgress(AccountSynchronizationProgress.Idle(accountId, SynchronizationProgressCategory.Mail));
|
||||
PublishSynchronizationProgress(AccountSynchronizationProgress.Idle(accountId, SynchronizationProgressCategory.Calendar));
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
@@ -679,6 +705,9 @@ public class SynchronizationManager : ISynchronizationManager
|
||||
_logger.Error(ex, "Failed to destroy synchronizer for account {AccountId}", accountId);
|
||||
}
|
||||
}
|
||||
|
||||
PublishSynchronizationProgress(AccountSynchronizationProgress.Idle(accountId, SynchronizationProgressCategory.Mail));
|
||||
PublishSynchronizationProgress(AccountSynchronizationProgress.Idle(accountId, SynchronizationProgressCategory.Calendar));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -770,6 +799,33 @@ public class SynchronizationManager : ISynchronizationManager
|
||||
}
|
||||
}
|
||||
|
||||
public void Receive(AccountSynchronizerStateChanged message)
|
||||
{
|
||||
var totalUnits = Math.Max(0, message.TotalItemsToSync);
|
||||
var remainingUnits = totalUnits > 0
|
||||
? Math.Clamp(message.RemainingItemsToSync, 0, totalUnits)
|
||||
: 0;
|
||||
|
||||
var isInProgress = message.NewState != AccountSynchronizerState.Idle;
|
||||
var isIndeterminate = isInProgress && totalUnits <= 0;
|
||||
var progressPercentage = totalUnits > 0
|
||||
? ((double)(totalUnits - remainingUnits) / totalUnits) * 100
|
||||
: 0;
|
||||
|
||||
var progress = new AccountSynchronizationProgress(
|
||||
message.AccountId,
|
||||
message.ProgressCategory,
|
||||
isInProgress,
|
||||
isIndeterminate,
|
||||
progressPercentage,
|
||||
totalUnits,
|
||||
remainingUnits,
|
||||
BuildSynchronizationStatus(message.ProgressCategory, message.NewState, totalUnits, progressPercentage, message.SynchronizationStatus),
|
||||
message.NewState);
|
||||
|
||||
PublishSynchronizationProgress(progress);
|
||||
}
|
||||
|
||||
private void EnsureInitialized()
|
||||
{
|
||||
if (!_isInitialized)
|
||||
@@ -804,17 +860,67 @@ public class SynchronizationManager : ISynchronizationManager
|
||||
return account?.AttentionReason == AccountAttentionReason.InvalidCredentials;
|
||||
}
|
||||
|
||||
private void PublishSynchronizationProgress(AccountSynchronizationProgress progress)
|
||||
{
|
||||
var normalized = progress.IsInProgress
|
||||
? progress
|
||||
: AccountSynchronizationProgress.Idle(progress.AccountId, progress.Category);
|
||||
|
||||
var cache = normalized.Category == SynchronizationProgressCategory.Calendar
|
||||
? _calendarSynchronizationProgress
|
||||
: _mailSynchronizationProgress;
|
||||
|
||||
cache.AddOrUpdate(normalized.AccountId, normalized, (_, _) => normalized);
|
||||
|
||||
WeakReferenceMessenger.Default.Send(new AccountSynchronizationProgressUpdatedMessage(normalized));
|
||||
}
|
||||
|
||||
private static string BuildSynchronizationStatus(
|
||||
SynchronizationProgressCategory category,
|
||||
AccountSynchronizerState state,
|
||||
int totalUnits,
|
||||
double progressPercentage,
|
||||
string rawStatus)
|
||||
{
|
||||
if (state == AccountSynchronizerState.Idle)
|
||||
return string.Empty;
|
||||
|
||||
if (state == AccountSynchronizerState.ExecutingRequests)
|
||||
return Translator.SynchronizationProgress_ApplyingChanges;
|
||||
|
||||
if (totalUnits > 0)
|
||||
{
|
||||
var roundedProgress = (int)Math.Round(progressPercentage, MidpointRounding.AwayFromZero);
|
||||
|
||||
return category == SynchronizationProgressCategory.Calendar
|
||||
? string.Format(Translator.SynchronizationProgress_CalendarPercent, roundedProgress)
|
||||
: string.Format(Translator.SynchronizationProgress_MailPercent, roundedProgress);
|
||||
}
|
||||
|
||||
if (category == SynchronizationProgressCategory.Calendar && !string.IsNullOrWhiteSpace(rawStatus))
|
||||
return rawStatus;
|
||||
|
||||
return category == SynchronizationProgressCategory.Calendar
|
||||
? Translator.SynchronizationProgress_CalendarInProgress
|
||||
: Translator.SynchronizationProgress_MailInProgress;
|
||||
}
|
||||
|
||||
private void PublishCalendarSynchronizationState(
|
||||
Guid accountId,
|
||||
CalendarSynchronizationType synchronizationType,
|
||||
bool isSynchronizationInProgress,
|
||||
string synchronizationStatus = "")
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send(new AccountCalendarSynchronizationStateChanged(
|
||||
PublishSynchronizationProgress(new AccountSynchronizationProgress(
|
||||
accountId,
|
||||
synchronizationType,
|
||||
SynchronizationProgressCategory.Calendar,
|
||||
isSynchronizationInProgress,
|
||||
synchronizationStatus));
|
||||
isSynchronizationInProgress,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
synchronizationStatus,
|
||||
isSynchronizationInProgress ? AccountSynchronizerState.Synchronizing : AccountSynchronizerState.Idle));
|
||||
}
|
||||
|
||||
private static string GetCalendarSynchronizationStatus(CalendarSynchronizationType synchronizationType)
|
||||
|
||||
@@ -27,6 +27,7 @@ public abstract partial class BaseSynchronizer<TBaseRequest> : ObservableObject,
|
||||
private readonly ConcurrentDictionary<Guid, byte> _pendingCalendarOperationIds = new();
|
||||
private readonly ConcurrentQueue<SynchronizationIssue> _capturedSynchronizationIssues = new();
|
||||
protected readonly IMessenger Messenger;
|
||||
protected SynchronizationProgressCategory CurrentSynchronizationProgressCategory { get; set; } = SynchronizationProgressCategory.Mail;
|
||||
|
||||
public MailAccount Account { get; }
|
||||
|
||||
@@ -44,7 +45,8 @@ public abstract partial class BaseSynchronizer<TBaseRequest> : ObservableObject,
|
||||
value,
|
||||
TotalItemsToSync,
|
||||
RemainingItemsToSync,
|
||||
SynchronizationStatus));
|
||||
SynchronizationStatus,
|
||||
CurrentSynchronizationProgressCategory));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,8 +77,8 @@ public abstract partial class BaseSynchronizer<TBaseRequest> : ObservableObject,
|
||||
{
|
||||
get
|
||||
{
|
||||
if (TotalItemsToSync == 0 || RemainingItemsToSync == 0)
|
||||
return -1; // Indeterminate
|
||||
if (TotalItemsToSync <= 0)
|
||||
return 0;
|
||||
|
||||
return ((double)(TotalItemsToSync - RemainingItemsToSync) / TotalItemsToSync) * 100;
|
||||
}
|
||||
@@ -118,7 +120,8 @@ public abstract partial class BaseSynchronizer<TBaseRequest> : ObservableObject,
|
||||
State,
|
||||
TotalItemsToSync,
|
||||
RemainingItemsToSync,
|
||||
SynchronizationStatus));
|
||||
SynchronizationStatus,
|
||||
CurrentSynchronizationProgressCategory));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -22,6 +22,7 @@ using Microsoft.IdentityModel.Tokens;
|
||||
using MimeKit;
|
||||
using MoreLinq;
|
||||
using Serilog;
|
||||
using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Entities.Calendar;
|
||||
using Wino.Core.Domain.Entities.Mail;
|
||||
using Wino.Core.Domain.Entities.Shared;
|
||||
@@ -388,7 +389,7 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
|
||||
} while (!string.IsNullOrEmpty(pageToken));
|
||||
|
||||
_logger.Information("Folder {FolderName}: Downloaded {Count} messages", folder.FolderName, folderDownloaded);
|
||||
UpdateSyncProgress(0, 0, $"Downloaded {totalMessagesDownloaded} messages");
|
||||
UpdateSyncProgress(totalFolders, 0, Translator.SyncAction_SynchronizingAccount);
|
||||
}
|
||||
|
||||
_logger.Information("Initial sync completed. Downloaded {Count} unique messages for {Name}", downloadedMessageIds.Count, Account.Name);
|
||||
@@ -521,8 +522,16 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
|
||||
.Where(c => c.IsSynchronizationEnabled)
|
||||
.ToList();
|
||||
|
||||
foreach (var calendar in localCalendars)
|
||||
var totalCalendars = localCalendars.Count;
|
||||
if (totalCalendars > 0)
|
||||
{
|
||||
UpdateSyncProgress(totalCalendars, totalCalendars, Translator.SyncAction_SynchronizingCalendarEvents);
|
||||
}
|
||||
|
||||
for (int i = 0; i < totalCalendars; i++)
|
||||
{
|
||||
var calendar = localCalendars[i];
|
||||
|
||||
try
|
||||
{
|
||||
var request = _calendarService.Events.List(calendar.RemoteCalendarId);
|
||||
@@ -602,6 +611,7 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
|
||||
}
|
||||
|
||||
await _gmailChangeProcessor.UpdateAccountCalendarAsync(calendar).ConfigureAwait(false);
|
||||
UpdateSyncProgress(totalCalendars, totalCalendars - (i + 1), Translator.SyncAction_SynchronizingCalendarEvents);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
@@ -624,6 +634,8 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
|
||||
|
||||
if (!errorContext.CanContinueSync)
|
||||
throw;
|
||||
|
||||
UpdateSyncProgress(totalCalendars, totalCalendars - (i + 1), Translator.SyncAction_SynchronizingCalendarEvents);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ using MailKit.Net.Imap;
|
||||
using MailKit.Search;
|
||||
using MimeKit;
|
||||
using Serilog;
|
||||
using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Entities.Calendar;
|
||||
using Wino.Core.Domain.Entities.Mail;
|
||||
using Wino.Core.Domain.Entities.Shared;
|
||||
@@ -1192,8 +1193,16 @@ public class ImapSynchronizer : WinoSynchronizer<ImapRequest, ImapMessageCreatio
|
||||
var periodStartUtc = DateTimeOffset.UtcNow.AddYears(-1);
|
||||
var periodEndUtc = DateTimeOffset.UtcNow.AddYears(2);
|
||||
|
||||
foreach (var localCalendar in localCalendars)
|
||||
var totalCalendars = localCalendars.Count;
|
||||
if (totalCalendars > 0)
|
||||
{
|
||||
UpdateSyncProgress(totalCalendars, totalCalendars, Translator.SyncAction_SynchronizingCalendarEvents);
|
||||
}
|
||||
|
||||
for (int i = 0; i < totalCalendars; i++)
|
||||
{
|
||||
var localCalendar = localCalendars[i];
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (!remoteCalendarsById.TryGetValue(localCalendar.RemoteCalendarId, out var remoteCalendar))
|
||||
@@ -1265,6 +1274,7 @@ public class ImapSynchronizer : WinoSynchronizer<ImapRequest, ImapMessageCreatio
|
||||
|
||||
localCalendar.SynchronizationDeltaToken = remoteToken;
|
||||
await _imapChangeProcessor.UpdateAccountCalendarAsync(localCalendar).ConfigureAwait(false);
|
||||
UpdateSyncProgress(totalCalendars, totalCalendars - (i + 1), Translator.SyncAction_SynchronizingCalendarEvents);
|
||||
}
|
||||
|
||||
return CalendarSynchronizationResult.Empty;
|
||||
|
||||
@@ -2320,8 +2320,16 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
||||
|
||||
// TODO: Maybe we can batch each calendar?
|
||||
|
||||
foreach (var calendar in localCalendars)
|
||||
var totalCalendars = localCalendars.Count;
|
||||
if (totalCalendars > 0)
|
||||
{
|
||||
UpdateSyncProgress(totalCalendars, totalCalendars, Translator.SyncAction_SynchronizingCalendarEvents);
|
||||
}
|
||||
|
||||
for (int i = 0; i < totalCalendars; i++)
|
||||
{
|
||||
var calendar = localCalendars[i];
|
||||
|
||||
try
|
||||
{
|
||||
bool isInitialSync = string.IsNullOrEmpty(calendar.SynchronizationDeltaToken);
|
||||
@@ -2440,6 +2448,8 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
||||
|
||||
await _outlookChangeProcessor.UpdateCalendarDeltaSynchronizationToken(calendar.Id, deltaToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
UpdateSyncProgress(totalCalendars, totalCalendars - (i + 1), Translator.SyncAction_SynchronizingCalendarEvents);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
@@ -2462,6 +2472,8 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
||||
|
||||
if (!errorContext.CanContinueSync)
|
||||
throw;
|
||||
|
||||
UpdateSyncProgress(totalCalendars, totalCalendars - (i + 1), Translator.SyncAction_SynchronizingCalendarEvents);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -147,6 +147,7 @@ public abstract class WinoSynchronizer<TBaseRequest, TMessageType, TCalendarEven
|
||||
|
||||
if (shouldExecuteRequests && changeRequestQueue.Any())
|
||||
{
|
||||
CurrentSynchronizationProgressCategory = SynchronizationProgressCategory.Mail;
|
||||
State = AccountSynchronizerState.ExecutingRequests;
|
||||
|
||||
List<IRequestBundle<TBaseRequest>> nativeRequests = new();
|
||||
@@ -264,6 +265,7 @@ public abstract class WinoSynchronizer<TBaseRequest, TMessageType, TCalendarEven
|
||||
await synchronizationSemaphore.WaitAsync(activeSynchronizationCancellationToken);
|
||||
|
||||
// Set indeterminate progress for initial state
|
||||
CurrentSynchronizationProgressCategory = SynchronizationProgressCategory.Mail;
|
||||
UpdateSyncProgress(0, 0, "Synchronizing...");
|
||||
|
||||
State = AccountSynchronizerState.Synchronizing;
|
||||
@@ -388,6 +390,7 @@ public abstract class WinoSynchronizer<TBaseRequest, TMessageType, TCalendarEven
|
||||
if (shouldExecuteRequests)
|
||||
{
|
||||
calendarRequestsWereExecuting = true;
|
||||
CurrentSynchronizationProgressCategory = SynchronizationProgressCategory.Calendar;
|
||||
State = AccountSynchronizerState.ExecutingRequests;
|
||||
|
||||
List<IRequestBundle<TBaseRequest>> nativeRequests = new();
|
||||
@@ -482,6 +485,7 @@ public abstract class WinoSynchronizer<TBaseRequest, TMessageType, TCalendarEven
|
||||
await Task.Delay(maxExecutionDelay, cancellationToken);
|
||||
}
|
||||
|
||||
CurrentSynchronizationProgressCategory = SynchronizationProgressCategory.Calendar;
|
||||
var synchronizationResult = await SynchronizeCalendarEventsInternalAsync(options, cancellationToken);
|
||||
return FinalizeCalendarResult(synchronizationResult);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user