Immidiate ui reflection for calendar events and some more error handling.
This commit is contained in:
@@ -11,6 +11,7 @@ using Wino.Core.Domain.Entities.Shared;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Accounts;
|
||||
using Wino.Core.Domain.Models.Synchronization;
|
||||
using Wino.Core.Requests.Bundles;
|
||||
using Wino.Messaging.UI;
|
||||
|
||||
@@ -24,6 +25,7 @@ public abstract partial class BaseSynchronizer<TBaseRequest> : ObservableObject,
|
||||
protected List<IRequestBase> changeRequestQueue = [];
|
||||
private readonly ConcurrentDictionary<Guid, byte> _pendingMailOperationIds = new();
|
||||
private readonly ConcurrentDictionary<Guid, byte> _pendingCalendarOperationIds = new();
|
||||
private readonly ConcurrentQueue<SynchronizationIssue> _capturedSynchronizationIssues = new();
|
||||
protected readonly IMessenger Messenger;
|
||||
|
||||
public MailAccount Account { get; }
|
||||
@@ -135,6 +137,8 @@ public abstract partial class BaseSynchronizer<TBaseRequest> : ObservableObject,
|
||||
|
||||
public bool HasPendingCalendarOperation(Guid calendarItemId) => _pendingCalendarOperationIds.ContainsKey(calendarItemId);
|
||||
|
||||
public IReadOnlyCollection<Guid> GetPendingCalendarOperationIds() => _pendingCalendarOperationIds.Keys.ToArray();
|
||||
|
||||
protected void TrackQueuedRequest(IRequestBase request)
|
||||
{
|
||||
if (request is IMailActionRequest mailActionRequest)
|
||||
@@ -173,6 +177,27 @@ public abstract partial class BaseSynchronizer<TBaseRequest> : ObservableObject,
|
||||
UntrackProcessedRequest(request);
|
||||
}
|
||||
|
||||
protected void ResetCapturedSynchronizationIssues()
|
||||
{
|
||||
while (_capturedSynchronizationIssues.TryDequeue(out _))
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
protected void CaptureSynchronizationIssue(SynchronizationIssue issue)
|
||||
{
|
||||
if (issue == null || string.IsNullOrWhiteSpace(issue.Message))
|
||||
return;
|
||||
|
||||
_capturedSynchronizationIssues.Enqueue(issue);
|
||||
}
|
||||
|
||||
protected void CaptureSynchronizationIssue(SynchronizerErrorContext errorContext)
|
||||
=> CaptureSynchronizationIssue(SynchronizationIssue.FromErrorContext(errorContext));
|
||||
|
||||
protected IReadOnlyList<SynchronizationIssue> GetCapturedSynchronizationIssues()
|
||||
=> _capturedSynchronizationIssues.ToArray();
|
||||
|
||||
/// <summary>
|
||||
/// Runs existing queued requests in the queue.
|
||||
/// </summary>
|
||||
|
||||
@@ -522,74 +522,110 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
|
||||
.Where(c => c.IsSynchronizationEnabled)
|
||||
.ToList();
|
||||
|
||||
// TODO: Better logging and exception handling.
|
||||
foreach (var calendar in localCalendars)
|
||||
{
|
||||
var request = _calendarService.Events.List(calendar.RemoteCalendarId);
|
||||
|
||||
// Fetch individual event instances (including recurring event occurrences)
|
||||
// rather than recurring event masters. This ensures we get all occurrences
|
||||
// as separate events that can be stored and displayed directly.
|
||||
request.SingleEvents = true;
|
||||
request.ShowDeleted = true;
|
||||
|
||||
if (!string.IsNullOrEmpty(calendar.SynchronizationDeltaToken))
|
||||
try
|
||||
{
|
||||
// If a sync token is available, perform an incremental sync
|
||||
request.SyncToken = calendar.SynchronizationDeltaToken;
|
||||
}
|
||||
else
|
||||
{
|
||||
// If no sync token, perform an initial sync
|
||||
// Fetch events from the past year
|
||||
var request = _calendarService.Events.List(calendar.RemoteCalendarId);
|
||||
|
||||
request.TimeMinDateTimeOffset = DateTimeOffset.UtcNow.AddYears(-1);
|
||||
}
|
||||
// Fetch individual event instances (including recurring event occurrences)
|
||||
// rather than recurring event masters. This ensures we get all occurrences
|
||||
// as separate events that can be stored and displayed directly.
|
||||
request.SingleEvents = true;
|
||||
request.ShowDeleted = true;
|
||||
|
||||
string nextPageToken;
|
||||
string syncToken;
|
||||
|
||||
var allEvents = new List<Event>();
|
||||
|
||||
do
|
||||
{
|
||||
// Execute the request
|
||||
var events = await request.ExecuteAsync();
|
||||
|
||||
// Process the fetched events
|
||||
if (events.Items != null)
|
||||
if (!string.IsNullOrEmpty(calendar.SynchronizationDeltaToken))
|
||||
{
|
||||
allEvents.AddRange(events.Items);
|
||||
request.SyncToken = calendar.SynchronizationDeltaToken;
|
||||
}
|
||||
else
|
||||
{
|
||||
request.TimeMinDateTimeOffset = DateTimeOffset.UtcNow.AddYears(-1);
|
||||
}
|
||||
|
||||
// Get the next page token and sync token
|
||||
nextPageToken = events.NextPageToken;
|
||||
syncToken = events.NextSyncToken;
|
||||
string nextPageToken;
|
||||
string syncToken;
|
||||
|
||||
// Set the next page token for subsequent requests
|
||||
request.PageToken = nextPageToken;
|
||||
var allEvents = new List<Event>();
|
||||
|
||||
} while (!string.IsNullOrEmpty(nextPageToken));
|
||||
do
|
||||
{
|
||||
var events = await request.ExecuteAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
calendar.SynchronizationDeltaToken = syncToken;
|
||||
if (events.Items != null)
|
||||
{
|
||||
allEvents.AddRange(events.Items);
|
||||
}
|
||||
|
||||
// allEvents contains new or updated events.
|
||||
// Process them and create/update local calendar items.
|
||||
nextPageToken = events.NextPageToken;
|
||||
syncToken = events.NextSyncToken;
|
||||
request.PageToken = nextPageToken;
|
||||
}
|
||||
while (!string.IsNullOrEmpty(nextPageToken));
|
||||
|
||||
var eventByRemoteId = allEvents
|
||||
.Where(e => !string.IsNullOrWhiteSpace(e.Id))
|
||||
.GroupBy(e => e.Id, StringComparer.Ordinal)
|
||||
.ToDictionary(g => g.Key, g => g.First(), StringComparer.Ordinal);
|
||||
calendar.SynchronizationDeltaToken = syncToken;
|
||||
|
||||
foreach (var @event in OrderCalendarEventsForPersistence(allEvents))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var eventByRemoteId = allEvents
|
||||
.Where(e => !string.IsNullOrWhiteSpace(e.Id))
|
||||
.GroupBy(e => e.Id, StringComparer.Ordinal)
|
||||
.ToDictionary(g => g.Key, g => g.First(), StringComparer.Ordinal);
|
||||
|
||||
await EnsureRecurringParentProcessedAsync(calendar, @event, eventByRemoteId, cancellationToken).ConfigureAwait(false);
|
||||
await _gmailChangeProcessor.ManageCalendarEventAsync(@event, calendar, Account).ConfigureAwait(false);
|
||||
foreach (var @event in OrderCalendarEventsForPersistence(allEvents))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
try
|
||||
{
|
||||
await EnsureRecurringParentProcessedAsync(calendar, @event, eventByRemoteId, cancellationToken).ConfigureAwait(false);
|
||||
await _gmailChangeProcessor.ManageCalendarEventAsync(@event, calendar, Account).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var errorContext = new SynchronizerErrorContext
|
||||
{
|
||||
Account = Account,
|
||||
ErrorMessage = ex.Message,
|
||||
Exception = ex,
|
||||
CalendarId = calendar.Id,
|
||||
CalendarName = calendar.Name,
|
||||
OperationType = "CalendarEventSync",
|
||||
Severity = SynchronizerErrorSeverity.Recoverable
|
||||
};
|
||||
|
||||
_ = await _gmailSynchronizerErrorHandlerFactory.HandleErrorAsync(errorContext).ConfigureAwait(false);
|
||||
CaptureSynchronizationIssue(errorContext);
|
||||
_logger.Error(ex, "Failed to process Gmail event {EventId} for calendar {CalendarName}", @event.Id, calendar.Name);
|
||||
}
|
||||
}
|
||||
|
||||
await _gmailChangeProcessor.UpdateAccountCalendarAsync(calendar).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var errorContext = new SynchronizerErrorContext
|
||||
{
|
||||
Account = Account,
|
||||
ErrorMessage = ex.Message,
|
||||
Exception = ex,
|
||||
CalendarId = calendar.Id,
|
||||
CalendarName = calendar.Name,
|
||||
OperationType = "CalendarSync"
|
||||
};
|
||||
|
||||
await _gmailChangeProcessor.UpdateAccountCalendarAsync(calendar).ConfigureAwait(false);
|
||||
_ = await _gmailSynchronizerErrorHandlerFactory.HandleErrorAsync(errorContext).ConfigureAwait(false);
|
||||
CaptureSynchronizationIssue(errorContext);
|
||||
|
||||
if (!errorContext.CanContinueSync)
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
return CalendarSynchronizationResult.Empty;
|
||||
@@ -1674,6 +1710,7 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
|
||||
ErrorCode = error.Code,
|
||||
ErrorMessage = error.Message,
|
||||
RequestBundle = bundle,
|
||||
Request = bundle.Request,
|
||||
IsEntityNotFound = isEntityNotFound,
|
||||
AdditionalData = new Dictionary<string, object>
|
||||
{
|
||||
@@ -1697,6 +1734,8 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
|
||||
// If not handled by any specific handler, apply default error handling
|
||||
if (!handled)
|
||||
{
|
||||
CaptureSynchronizationIssue(errorContext);
|
||||
|
||||
// OutOfMemoryException is a known bug in Gmail SDK.
|
||||
if (error.Code == 0)
|
||||
{
|
||||
|
||||
@@ -763,6 +763,7 @@ public class ImapSynchronizer : WinoSynchronizer<ImapRequest, ImapMessageCreatio
|
||||
ErrorMessage = ex.Message,
|
||||
Exception = ex,
|
||||
RequestBundle = item,
|
||||
Request = item.Request,
|
||||
OperationType = "RequestExecution",
|
||||
IsEntityNotFound = ex is FolderNotFoundException || ex is SynchronizerEntityNotFoundException
|
||||
};
|
||||
@@ -771,6 +772,8 @@ public class ImapSynchronizer : WinoSynchronizer<ImapRequest, ImapMessageCreatio
|
||||
|
||||
if (!handled)
|
||||
{
|
||||
CaptureSynchronizationIssue(errorContext);
|
||||
|
||||
if (ShouldApplyOptimisticUIChanges(item.Request))
|
||||
{
|
||||
item.Request.RevertUIChanges();
|
||||
|
||||
@@ -1861,6 +1861,7 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
||||
ErrorCode = (int)response.StatusCode,
|
||||
ErrorMessage = errorMessage,
|
||||
RequestBundle = bundle,
|
||||
Request = bundle.Request,
|
||||
IsEntityNotFound = IsKnownOutlookEntityNotFoundError(response.StatusCode, errorCode, errorMessage, bundle),
|
||||
AdditionalData = new Dictionary<string, object>
|
||||
{
|
||||
@@ -1876,6 +1877,7 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
||||
// Transient errors still need to bubble so the request can be retried or surfaced to the caller.
|
||||
if (!handled || errorContext.Severity == SynchronizerErrorSeverity.Transient)
|
||||
{
|
||||
CaptureSynchronizationIssue(errorContext);
|
||||
bundle.UIChangeRequest?.RevertUIChanges();
|
||||
Debug.WriteLine(errorString);
|
||||
errors.Add(errorString);
|
||||
@@ -2266,75 +2268,77 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
||||
|
||||
foreach (var calendar in localCalendars)
|
||||
{
|
||||
bool isInitialSync = string.IsNullOrEmpty(calendar.SynchronizationDeltaToken);
|
||||
|
||||
if (isInitialSync)
|
||||
try
|
||||
{
|
||||
_logger.Information("No calendar sync identifier for calendar {Name}. Performing initial sync.", calendar.Name);
|
||||
bool isInitialSync = string.IsNullOrEmpty(calendar.SynchronizationDeltaToken);
|
||||
|
||||
// ISO 8601 format as expected by Microsoft Graph API (e.g., "2019-11-08T19:00:00-08:00")
|
||||
var startDate = DateTimeOffset.Now.AddYears(-2).ToString("yyyy-MM-ddTHH:mm:sszzz");
|
||||
var endDate = DateTimeOffset.Now.AddYears(2).ToString("yyyy-MM-ddTHH:mm:sszzz");
|
||||
|
||||
// Get Id only. We will always download the full event.
|
||||
eventsDeltaResponse = await _graphClient.Me.Calendars[calendar.RemoteCalendarId].CalendarView.Delta.GetAsDeltaGetResponseAsync((requestConfiguration) =>
|
||||
if (isInitialSync)
|
||||
{
|
||||
requestConfiguration.QueryParameters.Select = ["id", "type"];
|
||||
requestConfiguration.QueryParameters.StartDateTime = startDate;
|
||||
requestConfiguration.QueryParameters.EndDateTime = endDate;
|
||||
}, cancellationToken: cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
var currentDeltaToken = calendar.SynchronizationDeltaToken;
|
||||
_logger.Information("No calendar sync identifier for calendar {Name}. Performing initial sync.", calendar.Name);
|
||||
|
||||
_logger.Information("Performing delta sync for calendar {Name}.", calendar.Name);
|
||||
// ISO 8601 format as expected by Microsoft Graph API (e.g., "2019-11-08T19:00:00-08:00")
|
||||
var startDate = DateTimeOffset.Now.AddYears(-2).ToString("yyyy-MM-ddTHH:mm:sszzz");
|
||||
var endDate = DateTimeOffset.Now.AddYears(2).ToString("yyyy-MM-ddTHH:mm:sszzz");
|
||||
|
||||
var requestInformation = _graphClient.Me.Calendars[calendar.RemoteCalendarId].CalendarView.Delta.ToGetRequestInformation();
|
||||
|
||||
requestInformation.UrlTemplate = requestInformation.UrlTemplate.Insert(requestInformation.UrlTemplate.Length - 1, ",%24deltatoken");
|
||||
requestInformation.QueryParameters.Add("%24deltatoken", currentDeltaToken);
|
||||
|
||||
eventsDeltaResponse = await _graphClient.RequestAdapter.SendAsync(requestInformation, Microsoft.Graph.Me.Calendars.Item.CalendarView.Delta.DeltaGetResponse.CreateFromDiscriminatorValue);
|
||||
}
|
||||
|
||||
List<Event> events = new();
|
||||
|
||||
// We must first save the parent recurring events to not lose exceptions.
|
||||
// Therefore, order the existing items by their type and save the parent recurring events first.
|
||||
|
||||
var messageIteratorAsync = PageIterator<Event, Microsoft.Graph.Me.Calendars.Item.CalendarView.Delta.DeltaGetResponse>.CreatePageIterator(_graphClient, eventsDeltaResponse, (item) =>
|
||||
{
|
||||
// Include all event types: SingleInstance, SeriesMaster, Occurrence, and Exception
|
||||
// CalendarView already expands recurring events into individual occurrences
|
||||
events.Add(item);
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
await messageIteratorAsync
|
||||
.IterateAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
// Desc-order will move parent recurring events to the top.
|
||||
events = events.OrderByDescending(a => a.Type).ToList();
|
||||
|
||||
_logger.Information("Found {Count} events in total.", events.Count);
|
||||
|
||||
foreach (var item in events)
|
||||
{
|
||||
// Declined events are returned as Deleted from the API.
|
||||
// There is no way to distinguish unfortunately atm.
|
||||
|
||||
if (IsResourceDeleted(item.AdditionalData))
|
||||
{
|
||||
await _outlookChangeProcessor.DeleteCalendarItemAsync(item.Id, calendar.Id).ConfigureAwait(false);
|
||||
// Get Id only. We will always download the full event.
|
||||
eventsDeltaResponse = await _graphClient.Me.Calendars[calendar.RemoteCalendarId].CalendarView.Delta.GetAsDeltaGetResponseAsync((requestConfiguration) =>
|
||||
{
|
||||
requestConfiguration.QueryParameters.Select = ["id", "type"];
|
||||
requestConfiguration.QueryParameters.StartDateTime = startDate;
|
||||
requestConfiguration.QueryParameters.EndDateTime = endDate;
|
||||
}, cancellationToken: cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
var currentDeltaToken = calendar.SynchronizationDeltaToken;
|
||||
|
||||
_logger.Information("Performing delta sync for calendar {Name}.", calendar.Name);
|
||||
|
||||
var requestInformation = _graphClient.Me.Calendars[calendar.RemoteCalendarId].CalendarView.Delta.ToGetRequestInformation();
|
||||
|
||||
requestInformation.UrlTemplate = requestInformation.UrlTemplate.Insert(requestInformation.UrlTemplate.Length - 1, ",%24deltatoken");
|
||||
requestInformation.QueryParameters.Add("%24deltatoken", currentDeltaToken);
|
||||
|
||||
eventsDeltaResponse = await _graphClient.RequestAdapter.SendAsync(requestInformation, Microsoft.Graph.Me.Calendars.Item.CalendarView.Delta.DeltaGetResponse.CreateFromDiscriminatorValue);
|
||||
}
|
||||
|
||||
List<Event> events = new();
|
||||
|
||||
// We must first save the parent recurring events to not lose exceptions.
|
||||
// Therefore, order the existing items by their type and save the parent recurring events first.
|
||||
|
||||
var messageIteratorAsync = PageIterator<Event, Microsoft.Graph.Me.Calendars.Item.CalendarView.Delta.DeltaGetResponse>.CreatePageIterator(_graphClient, eventsDeltaResponse, (item) =>
|
||||
{
|
||||
// Include all event types: SingleInstance, SeriesMaster, Occurrence, and Exception
|
||||
// CalendarView already expands recurring events into individual occurrences
|
||||
events.Add(item);
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
await messageIteratorAsync
|
||||
.IterateAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
// Desc-order will move parent recurring events to the top.
|
||||
events = events.OrderByDescending(a => a.Type).ToList();
|
||||
|
||||
_logger.Information("Found {Count} events in total.", events.Count);
|
||||
|
||||
foreach (var item in events)
|
||||
{
|
||||
// Declined events are returned as Deleted from the API.
|
||||
// There is no way to distinguish unfortunately atm.
|
||||
|
||||
if (IsResourceDeleted(item.AdditionalData))
|
||||
{
|
||||
await _outlookChangeProcessor.DeleteCalendarItemAsync(item.Id, calendar.Id).ConfigureAwait(false);
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await _handleCalendarEventRetrievalSemaphore.WaitAsync();
|
||||
await _handleCalendarEventRetrievalSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
Event fullEvent = await _graphClient.Me.Calendars[calendar.RemoteCalendarId].Events[item.Id]
|
||||
.GetAsync(requestConfiguration =>
|
||||
@@ -2344,8 +2348,25 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
||||
}, cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||
await _outlookChangeProcessor.ManageCalendarEventAsync(fullEvent, calendar, Account).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var errorContext = new SynchronizerErrorContext
|
||||
{
|
||||
Account = Account,
|
||||
ErrorMessage = ex.Message,
|
||||
Exception = ex,
|
||||
CalendarId = calendar.Id,
|
||||
CalendarName = calendar.Name,
|
||||
OperationType = "CalendarEventSync",
|
||||
Severity = SynchronizerErrorSeverity.Recoverable
|
||||
};
|
||||
|
||||
_ = await _errorHandlingFactory.HandleErrorAsync(errorContext).ConfigureAwait(false);
|
||||
CaptureSynchronizationIssue(errorContext);
|
||||
_logger.Error(ex, "Error occurred while handling item {Id} for calendar {Name}", item.Id, calendar.Name);
|
||||
}
|
||||
finally
|
||||
@@ -2353,18 +2374,40 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
||||
_handleCalendarEventRetrievalSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
var latestDeltaLink = messageIteratorAsync.Deltalink;
|
||||
|
||||
//Store delta link for tracking new changes.
|
||||
if (!string.IsNullOrEmpty(latestDeltaLink))
|
||||
{
|
||||
// Parse Delta Token from Delta Link since v5 of Graph SDK works based on the token, not the link.
|
||||
|
||||
var deltaToken = GetDeltaTokenFromDeltaLink(latestDeltaLink);
|
||||
|
||||
await _outlookChangeProcessor.UpdateCalendarDeltaSynchronizationToken(calendar.Id, deltaToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
var latestDeltaLink = messageIteratorAsync.Deltalink;
|
||||
|
||||
//Store delta link for tracking new changes.
|
||||
if (!string.IsNullOrEmpty(latestDeltaLink))
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Parse Delta Token from Delta Link since v5 of Graph SDK works based on the token, not the link.
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var errorContext = new SynchronizerErrorContext
|
||||
{
|
||||
Account = Account,
|
||||
ErrorMessage = ex.Message,
|
||||
Exception = ex,
|
||||
CalendarId = calendar.Id,
|
||||
CalendarName = calendar.Name,
|
||||
OperationType = "CalendarSync"
|
||||
};
|
||||
|
||||
var deltaToken = GetDeltaTokenFromDeltaLink(latestDeltaLink);
|
||||
_ = await _errorHandlingFactory.HandleErrorAsync(errorContext).ConfigureAwait(false);
|
||||
CaptureSynchronizationIssue(errorContext);
|
||||
|
||||
await _outlookChangeProcessor.UpdateCalendarDeltaSynchronizationToken(calendar.Id, deltaToken).ConfigureAwait(false);
|
||||
if (!errorContext.CanContinueSync)
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Entities.Mail;
|
||||
using Wino.Core.Domain.Entities.Shared;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Exceptions;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Accounts;
|
||||
using Wino.Core.Domain.Models.Folders;
|
||||
@@ -121,12 +122,15 @@ public abstract class WinoSynchronizer<TBaseRequest, TMessageType, TCalendarEven
|
||||
/// <returns>Synchronization result that contains summary of the sync.</returns>
|
||||
public async Task<MailSynchronizationResult> SynchronizeMailsAsync(MailSynchronizationOptions options, CancellationToken cancellationToken = default)
|
||||
{
|
||||
ResetCapturedSynchronizationIssues();
|
||||
List<IRequestBase> requestCopies = null;
|
||||
|
||||
try
|
||||
{
|
||||
if (!ShouldQueueMailSynchronization(options))
|
||||
{
|
||||
Log.Debug($"{options.Type} synchronization is ignored.");
|
||||
return MailSynchronizationResult.Canceled;
|
||||
return FinalizeMailResult(MailSynchronizationResult.Canceled);
|
||||
}
|
||||
|
||||
var newCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
@@ -147,7 +151,7 @@ public abstract class WinoSynchronizer<TBaseRequest, TMessageType, TCalendarEven
|
||||
|
||||
List<IRequestBundle<TBaseRequest>> nativeRequests = new();
|
||||
|
||||
List<IRequestBase> requestCopies = new(changeRequestQueue);
|
||||
requestCopies = new(changeRequestQueue);
|
||||
|
||||
var keys = changeRequestQueue.GroupBy(a => a.GroupingKey());
|
||||
|
||||
@@ -226,10 +230,9 @@ public abstract class WinoSynchronizer<TBaseRequest, TMessageType, TCalendarEven
|
||||
finally
|
||||
{
|
||||
UntrackProcessedRequests(requestCopies);
|
||||
Messenger.Send(new SynchronizationActionsCompleted(Account.Id));
|
||||
}
|
||||
|
||||
Messenger.Send(new SynchronizationActionsCompleted(Account.Id));
|
||||
|
||||
PublishUnreadItemChanges();
|
||||
|
||||
// Execute request sync options should be re-calculated after execution.
|
||||
@@ -275,14 +278,19 @@ public abstract class WinoSynchronizer<TBaseRequest, TMessageType, TCalendarEven
|
||||
{
|
||||
newProfileInformation = await SynchronizeProfileInformationInternalAsync();
|
||||
}
|
||||
catch (AuthenticationAttentionException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Failed to update profile information for {Name}", Account.Name);
|
||||
|
||||
return MailSynchronizationResult.Failed(ex);
|
||||
CaptureSynchronizationIssue(SynchronizationIssue.FromException(ex, "ProfileSync"));
|
||||
return FinalizeMailResult(MailSynchronizationResult.Failed(ex));
|
||||
}
|
||||
|
||||
return MailSynchronizationResult.Completed(newProfileInformation);
|
||||
return FinalizeMailResult(MailSynchronizationResult.Completed(newProfileInformation));
|
||||
}
|
||||
|
||||
// Alias sync.
|
||||
@@ -294,13 +302,18 @@ public abstract class WinoSynchronizer<TBaseRequest, TMessageType, TCalendarEven
|
||||
{
|
||||
await SynchronizeAliasesAsync();
|
||||
|
||||
return MailSynchronizationResult.Empty;
|
||||
return FinalizeMailResult(MailSynchronizationResult.Empty);
|
||||
}
|
||||
catch (AuthenticationAttentionException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Failed to update aliases for {Name}", Account.Name);
|
||||
|
||||
return MailSynchronizationResult.Failed(ex);
|
||||
CaptureSynchronizationIssue(SynchronizationIssue.FromException(ex, "AliasSync"));
|
||||
return FinalizeMailResult(MailSynchronizationResult.Failed(ex));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -314,19 +327,23 @@ public abstract class WinoSynchronizer<TBaseRequest, TMessageType, TCalendarEven
|
||||
|
||||
PublishUnreadItemChanges();
|
||||
|
||||
return synchronizationResult;
|
||||
return FinalizeMailResult(synchronizationResult);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
Logger.Warning("Synchronization canceled.");
|
||||
|
||||
return MailSynchronizationResult.Canceled;
|
||||
return FinalizeMailResult(MailSynchronizationResult.Canceled);
|
||||
}
|
||||
catch (AuthenticationAttentionException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error(ex, "Synchronization failed for {Name}", Account.Name);
|
||||
|
||||
throw;
|
||||
CaptureSynchronizationIssue(SynchronizationIssue.FromException(ex, "MailSync"));
|
||||
return FinalizeMailResult(MailSynchronizationResult.Failed(ex));
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -355,104 +372,132 @@ public abstract class WinoSynchronizer<TBaseRequest, TMessageType, TCalendarEven
|
||||
/// <returns>Synchronization result that contains summary of the sync.</returns>
|
||||
public async Task<CalendarSynchronizationResult> SynchronizeCalendarEventsAsync(CalendarSynchronizationOptions options, CancellationToken cancellationToken = default)
|
||||
{
|
||||
bool shouldExecuteRequests = changeRequestQueue.Any(r => r is ICalendarActionRequest);
|
||||
bool shouldDelayExecution = false;
|
||||
int maxExecutionDelay = 0;
|
||||
ResetCapturedSynchronizationIssues();
|
||||
List<IRequestBase> requestCopies = null;
|
||||
var calendarRequestsWereExecuting = false;
|
||||
|
||||
if (shouldExecuteRequests)
|
||||
try
|
||||
{
|
||||
State = AccountSynchronizerState.ExecutingRequests;
|
||||
bool shouldExecuteRequests = changeRequestQueue.Any(r => r is ICalendarActionRequest);
|
||||
bool shouldDelayExecution = false;
|
||||
int maxExecutionDelay = 0;
|
||||
|
||||
List<IRequestBundle<TBaseRequest>> nativeRequests = new();
|
||||
List<IRequestBase> requestCopies = new(changeRequestQueue.Where(r => r is ICalendarActionRequest));
|
||||
|
||||
var keys = requestCopies.GroupBy(a => a.GroupingKey());
|
||||
|
||||
foreach (var group in keys)
|
||||
if (shouldExecuteRequests)
|
||||
{
|
||||
var key = group.Key;
|
||||
calendarRequestsWereExecuting = true;
|
||||
State = AccountSynchronizerState.ExecutingRequests;
|
||||
|
||||
if (key is CalendarSynchronizerOperation calendarSynchronizerOperation)
|
||||
List<IRequestBundle<TBaseRequest>> nativeRequests = new();
|
||||
requestCopies = new(changeRequestQueue.Where(r => r is ICalendarActionRequest));
|
||||
|
||||
var keys = requestCopies.GroupBy(a => a.GroupingKey());
|
||||
|
||||
foreach (var group in keys)
|
||||
{
|
||||
switch (calendarSynchronizerOperation)
|
||||
var key = group.Key;
|
||||
|
||||
if (key is CalendarSynchronizerOperation calendarSynchronizerOperation)
|
||||
{
|
||||
case CalendarSynchronizerOperation.CreateEvent:
|
||||
nativeRequests.AddRange(group
|
||||
.OfType<CreateCalendarEventRequest>()
|
||||
.SelectMany(CreateCalendarEvent));
|
||||
break;
|
||||
case CalendarSynchronizerOperation.AcceptEvent:
|
||||
nativeRequests.AddRange(group
|
||||
.OfType<AcceptEventRequest>()
|
||||
.SelectMany(AcceptEvent));
|
||||
break;
|
||||
case CalendarSynchronizerOperation.DeclineEvent:
|
||||
if (Account.ProviderType == MailProviderType.Outlook)
|
||||
{
|
||||
switch (calendarSynchronizerOperation)
|
||||
{
|
||||
case CalendarSynchronizerOperation.CreateEvent:
|
||||
nativeRequests.AddRange(group
|
||||
.OfType<OutlookDeclineEventRequest>()
|
||||
.SelectMany(OutlookDeclineEvent));
|
||||
}
|
||||
else
|
||||
{
|
||||
.OfType<CreateCalendarEventRequest>()
|
||||
.SelectMany(CreateCalendarEvent));
|
||||
break;
|
||||
case CalendarSynchronizerOperation.AcceptEvent:
|
||||
nativeRequests.AddRange(group
|
||||
.OfType<DeclineEventRequest>()
|
||||
.SelectMany(DeclineEvent));
|
||||
}
|
||||
break;
|
||||
case CalendarSynchronizerOperation.TentativeEvent:
|
||||
nativeRequests.AddRange(group
|
||||
.OfType<TentativeEventRequest>()
|
||||
.SelectMany(TentativeEvent));
|
||||
break;
|
||||
case CalendarSynchronizerOperation.UpdateEvent:
|
||||
nativeRequests.AddRange(group
|
||||
.OfType<UpdateCalendarEventRequest>()
|
||||
.SelectMany(UpdateCalendarEvent));
|
||||
break;
|
||||
case CalendarSynchronizerOperation.DeleteEvent:
|
||||
nativeRequests.AddRange(group
|
||||
.OfType<DeleteCalendarEventRequest>()
|
||||
.SelectMany(DeleteCalendarEvent));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
.OfType<AcceptEventRequest>()
|
||||
.SelectMany(AcceptEvent));
|
||||
break;
|
||||
case CalendarSynchronizerOperation.DeclineEvent:
|
||||
if (Account.ProviderType == MailProviderType.Outlook)
|
||||
{
|
||||
nativeRequests.AddRange(group
|
||||
.OfType<OutlookDeclineEventRequest>()
|
||||
.SelectMany(OutlookDeclineEvent));
|
||||
}
|
||||
else
|
||||
{
|
||||
nativeRequests.AddRange(group
|
||||
.OfType<DeclineEventRequest>()
|
||||
.SelectMany(DeclineEvent));
|
||||
}
|
||||
break;
|
||||
case CalendarSynchronizerOperation.TentativeEvent:
|
||||
nativeRequests.AddRange(group
|
||||
.OfType<TentativeEventRequest>()
|
||||
.SelectMany(TentativeEvent));
|
||||
break;
|
||||
case CalendarSynchronizerOperation.UpdateEvent:
|
||||
nativeRequests.AddRange(group
|
||||
.OfType<UpdateCalendarEventRequest>()
|
||||
.SelectMany(UpdateCalendarEvent));
|
||||
break;
|
||||
case CalendarSynchronizerOperation.DeleteEvent:
|
||||
nativeRequests.AddRange(group
|
||||
.OfType<DeleteCalendarEventRequest>()
|
||||
.SelectMany(DeleteCalendarEvent));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove processed calendar requests from queue
|
||||
changeRequestQueue.RemoveAll(r => r is ICalendarActionRequest);
|
||||
|
||||
Console.WriteLine($"Prepared {nativeRequests.Count()} native calendar requests");
|
||||
|
||||
try
|
||||
{
|
||||
await ExecuteNativeRequestsAsync(nativeRequests, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
UntrackProcessedRequests(requestCopies);
|
||||
Messenger.Send(new SynchronizationActionsCompleted(Account.Id));
|
||||
}
|
||||
|
||||
// Let servers to finish their job. Sometimes the servers don't respond immediately.
|
||||
shouldDelayExecution = requestCopies.Any(a => a.ResynchronizationDelay > 0);
|
||||
|
||||
if (shouldDelayExecution)
|
||||
{
|
||||
maxExecutionDelay = requestCopies.Aggregate(0, (max, next) => Math.Max(max, next.ResynchronizationDelay));
|
||||
}
|
||||
}
|
||||
|
||||
// Remove processed calendar requests from queue
|
||||
changeRequestQueue.RemoveAll(r => r is ICalendarActionRequest);
|
||||
|
||||
Console.WriteLine($"Prepared {nativeRequests.Count()} native calendar requests");
|
||||
|
||||
try
|
||||
{
|
||||
await ExecuteNativeRequestsAsync(nativeRequests, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
UntrackProcessedRequests(requestCopies);
|
||||
}
|
||||
|
||||
Messenger.Send(new SynchronizationActionsCompleted(Account.Id));
|
||||
|
||||
// Let servers to finish their job. Sometimes the servers don't respond immediately.
|
||||
shouldDelayExecution = requestCopies.Any(a => a.ResynchronizationDelay > 0);
|
||||
|
||||
if (shouldDelayExecution)
|
||||
{
|
||||
maxExecutionDelay = requestCopies.Aggregate(0, (max, next) => Math.Max(max, next.ResynchronizationDelay));
|
||||
await Task.Delay(maxExecutionDelay, cancellationToken);
|
||||
}
|
||||
|
||||
var synchronizationResult = await SynchronizeCalendarEventsInternalAsync(options, cancellationToken);
|
||||
return FinalizeCalendarResult(synchronizationResult);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return FinalizeCalendarResult(CalendarSynchronizationResult.Canceled);
|
||||
}
|
||||
catch (AuthenticationAttentionException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
CaptureSynchronizationIssue(SynchronizationIssue.FromException(ex, "CalendarSync"));
|
||||
return FinalizeCalendarResult(CalendarSynchronizationResult.Failed(ex));
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (calendarRequestsWereExecuting && State == AccountSynchronizerState.ExecutingRequests)
|
||||
{
|
||||
ResetSyncProgress();
|
||||
State = AccountSynchronizerState.Idle;
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldDelayExecution)
|
||||
{
|
||||
await Task.Delay(maxExecutionDelay, cancellationToken);
|
||||
}
|
||||
|
||||
// Execute the actual synchronization
|
||||
return await SynchronizeCalendarEventsInternalAsync(options, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -626,4 +671,10 @@ public abstract class WinoSynchronizer<TBaseRequest, TMessageType, TCalendarEven
|
||||
request.Value.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private MailSynchronizationResult FinalizeMailResult(MailSynchronizationResult result)
|
||||
=> (result ?? MailSynchronizationResult.Empty).MergeIssues(GetCapturedSynchronizationIssues());
|
||||
|
||||
private CalendarSynchronizationResult FinalizeCalendarResult(CalendarSynchronizationResult result)
|
||||
=> (result ?? CalendarSynchronizationResult.Empty).MergeIssues(GetCapturedSynchronizationIssues());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user