From 268381d3f16763e3ef3d06a5aa2c9fde72d90fd9 Mon Sep 17 00:00:00 2001 From: Kevin Jump Date: Tue, 25 Aug 2026 16:34:22 +0100 Subject: [PATCH] Add non-breaking async callback surface to uSyncCallbacks The uSyncCallbacks delegates (Callback, Update, SetRange, IncrementalUpdate, Complete) were void, which forced consumers like uSync.Complete's LocalHubClient/PublisherHubClient to .Wait() on every SignalR send. Add optional async counterparts (CallbackAsync, UpdateAsync, SetRangeAsync, IncrementalUpdateAsync, CompleteAsync) alongside the existing sync delegates, plus Raise*Async helper methods that invoke the sync delegate then await the async one if set. Existing constructors, properties and delegate types are untouched, so this is purely additive. Update the call sites that invoke callbacks directly off a uSyncCallbacks instance (SyncService, SyncService_Single, SyncActionService, SyncHandlerRoot, uSyncManagementService) to await the new Raise*Async helpers. Call sites that extract the raw SyncUpdateCallback delegate to pass into the public ISyncHandler.ExportAllAsync/ReportAsync interface are left untouched, since changing that interface would be a breaking change. --- uSync.BackOffice/Hubs/uSyncCallbacks.cs | 117 +++++++++++++++++- .../Services/SyncActionService.cs | 6 +- uSync.BackOffice/Services/SyncService.cs | 22 ++-- .../Services/SyncService_Single.cs | 21 ++-- .../SyncHandlers/SyncHandlerRoot.cs | 18 ++- .../Services/uSyncManagementService.cs | 15 ++- 6 files changed, 166 insertions(+), 33 deletions(-) diff --git a/uSync.BackOffice/Hubs/uSyncCallbacks.cs b/uSync.BackOffice/Hubs/uSyncCallbacks.cs index 6f6fdfaf3..77b0bf368 100644 --- a/uSync.BackOffice/Hubs/uSyncCallbacks.cs +++ b/uSync.BackOffice/Hubs/uSyncCallbacks.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Threading.Tasks; using uSync.BackOffice.Models; @@ -11,26 +12,52 @@ namespace uSync.BackOffice; public delegate void SyncEventCallback(SyncProgressSummary summary); /// -/// callback delegate for SignalR messaging +/// async callback delegate for SignalR messaging +/// +public delegate Task SyncEventCallbackAsync(SyncProgressSummary summary); + +/// +/// callback delegate for SignalR messaging /// public delegate void SyncUpdateCallback(string message, int count, int total); +/// +/// async callback delegate for SignalR messaging +/// +public delegate Task SyncUpdateCallbackAsync(string message, int count, int total); + /// /// callback delegate to set the start and end range for the update counters. /// public delegate void SyncSetUpdateRange(int start, int end); +/// +/// async callback delegate to set the start and end range for the update counters. +/// +public delegate Task SyncSetUpdateRangeAsync(int start, int end); + /// /// callback to send a update message and increment the counter by one so moving the progress bar. /// /// public delegate void SyncIncrementalUpdateCallback(string message); +/// +/// async callback to send a update message and increment the counter by one so moving the progress bar. +/// +/// +public delegate Task SyncIncrementalUpdateCallbackAsync(string message); + /// /// callback to signal that the sync is complete /// public delegate void SyncCompleteCallBack(Guid id, string message, bool success, IEnumerable actions); +/// +/// async callback to signal that the sync is complete +/// +public delegate Task SyncCompleteCallBackAsync(Guid id, string message, bool success, IEnumerable actions); + /// /// Callback objects used to communicate via SignalR @@ -42,29 +69,56 @@ public class uSyncCallbacks /// public SyncEventCallback? Callback { get; private set; } + /// + /// Async add event callback. Set alongside (or instead of) when the + /// consumer needs to await the send (e.g. a SignalR hub push) rather than block on it. + /// + public SyncEventCallbackAsync? CallbackAsync { get; set; } + /// /// Update event callback /// public SyncUpdateCallback? Update { get; private set; } + /// + /// Async update event callback. Set alongside (or instead of) when the + /// consumer needs to await the send rather than block on it. + /// + public SyncUpdateCallbackAsync? UpdateAsync { get; set; } + /// /// set a start and end range for the counter. /// public SyncSetUpdateRange? SetRange { get; private set; } + /// + /// Async version of . + /// + public SyncSetUpdateRangeAsync? SetRangeAsync { get; set; } + /// /// update and increment callback. /// public SyncIncrementalUpdateCallback? IncrementalUpdate { get; private set; } + /// + /// Async version of . + /// + public SyncIncrementalUpdateCallbackAsync? IncrementalUpdateAsync { get; set; } + /// /// callback to signal that the sync is complete /// public SyncCompleteCallBack? Complete { get; private set; } + /// + /// Async version of . + /// + public SyncCompleteCallBackAsync? CompleteAsync { get; set; } + /// - /// generate a new callback object + /// generate a new callback object /// public uSyncCallbacks(SyncEventCallback? callback, SyncUpdateCallback? update) { @@ -75,9 +129,9 @@ public uSyncCallbacks(SyncEventCallback? callback, SyncUpdateCallback? update) /// /// generate a callback object with range and incremental update /// - public uSyncCallbacks(SyncEventCallback? callback, + public uSyncCallbacks(SyncEventCallback? callback, SyncUpdateCallback? update, - SyncSetUpdateRange? updateRange, + SyncSetUpdateRange? updateRange, SyncIncrementalUpdateCallback incrementalUpdate, SyncCompleteCallBack? complete) : this(callback, update) @@ -86,4 +140,59 @@ public uSyncCallbacks(SyncEventCallback? callback, this.IncrementalUpdate = incrementalUpdate; this.Complete = complete; } + + /// + /// raise the / event, awaiting the async + /// version (if set) so callers no longer need to block on it. + /// + public async Task RaiseCallbackAsync(SyncProgressSummary summary) + { + Callback?.Invoke(summary); + if (CallbackAsync is not null) + await CallbackAsync(summary).ConfigureAwait(false); + } + + /// + /// raise the / event, awaiting the async + /// version (if set) so callers no longer need to block on it. + /// + public async Task RaiseUpdateAsync(string message, int count, int total) + { + Update?.Invoke(message, count, total); + if (UpdateAsync is not null) + await UpdateAsync(message, count, total).ConfigureAwait(false); + } + + /// + /// raise the / event, awaiting the async + /// version (if set) so callers no longer need to block on it. + /// + public async Task RaiseSetRangeAsync(int start, int end) + { + SetRange?.Invoke(start, end); + if (SetRangeAsync is not null) + await SetRangeAsync(start, end).ConfigureAwait(false); + } + + /// + /// raise the / event, awaiting + /// the async version (if set) so callers no longer need to block on it. + /// + public async Task RaiseIncrementalUpdateAsync(string message) + { + IncrementalUpdate?.Invoke(message); + if (IncrementalUpdateAsync is not null) + await IncrementalUpdateAsync(message).ConfigureAwait(false); + } + + /// + /// raise the / event, awaiting the async + /// version (if set) so callers no longer need to block on it. + /// + public async Task RaiseCompleteAsync(Guid id, string message, bool success, IEnumerable actions) + { + Complete?.Invoke(id, message, success, actions); + if (CompleteAsync is not null) + await CompleteAsync(id, message, success, actions).ConfigureAwait(false); + } } diff --git a/uSync.BackOffice/Services/SyncActionService.cs b/uSync.BackOffice/Services/SyncActionService.cs index 91310959b..afe499d66 100644 --- a/uSync.BackOffice/Services/SyncActionService.cs +++ b/uSync.BackOffice/Services/SyncActionService.cs @@ -124,7 +124,8 @@ public async Task ImportPostAsync(SyncFinalActionRequest reque request.ActionOptions.GetSetOrDefault(_uSyncConfig.Settings.DefaultSet), request.Actions); - request.Callbacks?.Update?.Invoke("Post Import Complete", 1, 1); + if (request.Callbacks is not null) + await request.Callbacks.RaiseUpdateAsync("Post Import Complete", 1, 1); return new SyncActionResult([.. actions.Where(x => x.Change > Core.ChangeType.NoChange)]); } @@ -218,7 +219,8 @@ public async Task FinishProcessAsync(SyncFinalActionRequest re request.HandlerAction, request.Actions.CountChanges(), request.Actions.Count(), elapsed); } - request.Callbacks?.Update?.Invoke($"{request.HandlerAction} completed ({elapsed:#,#}ms)", 1, 1); + if (request.Callbacks is not null) + await request.Callbacks.RaiseUpdateAsync($"{request.HandlerAction} completed ({elapsed:#,#}ms)", 1, 1); // for speed we return an empty list. the merge will just take // what we where passed in, and we avoid a whole copy and compare step diff --git a/uSync.BackOffice/Services/SyncService.cs b/uSync.BackOffice/Services/SyncService.cs index bd0d31a03..0a2db7482 100644 --- a/uSync.BackOffice/Services/SyncService.cs +++ b/uSync.BackOffice/Services/SyncService.cs @@ -198,7 +198,8 @@ public async Task> ImportAsync(string[] folders, bool f summary.UpdateHandler( handler.Name, HandlerStatus.Processing, $"Importing {handler.Name}", 0); - callbacks?.Callback?.Invoke(summary); + if (callbacks is not null) + await callbacks.RaiseCallbackAsync(summary); var handlerActions = await this.ImportHandlerAsync(handler.Alias, importOptions); @@ -228,14 +229,18 @@ public async Task> ImportAsync(string[] folders, bool f if (actions.ContainsErrors()) _logger.LogWarning("uSync Import: Errors detected in import : {count}", actions.CountErrors()); - callbacks?.Update?.Invoke($"Processed {actions.Count} items in {sw.ElapsedMilliseconds}ms", 1, 1); + if (callbacks is not null) + await callbacks.RaiseUpdateAsync($"Processed {actions.Count} items in {sw.ElapsedMilliseconds}ms", 1, 1); summary.Message = "Completed"; summary.Total = handlers.Count() + 1; var finalActions = actions.Select(x => x.AsActionView()).ToList(); - callbacks?.Callback?.Invoke(summary); - callbacks?.Complete?.Invoke(requestId, "Sync complete", true, finalActions); + if (callbacks is not null) + { + await callbacks.RaiseCallbackAsync(summary); + await callbacks.RaiseCompleteAsync(requestId, "Sync complete", true, finalActions); + } return actions; } @@ -392,7 +397,8 @@ public async Task> ExportAsync(string folder, IEnumerab summary.UpdateHandler( handler.Name, HandlerStatus.Processing, $"Exporting {handler.Name}", 0); - callbacks?.Callback?.Invoke(summary); + if (callbacks is not null) + await callbacks.RaiseCallbackAsync(summary); var handlerActions = await handler.ExportAllAsync([$"{folder}/{handler.DefaultFolder}"], configuredHandler.Settings, callbacks?.Update); @@ -405,7 +411,8 @@ public async Task> ExportAsync(string folder, IEnumerab summary.UpdateMessage("Export Completed"); - callbacks?.Callback?.Invoke(summary); + if (callbacks is not null) + await callbacks.RaiseCallbackAsync(summary); await _mutexService.FireBulkCompleteAsync(new uSyncExportCompletedNotification(actions, null)); @@ -419,7 +426,8 @@ public async Task> ExportAsync(string folder, IEnumerab sw.ElapsedMilliseconds); } - callbacks?.Update?.Invoke($"Processed {actions.Count} items in {sw.ElapsedMilliseconds}ms", 1, 1); + if (callbacks is not null) + await callbacks.RaiseUpdateAsync($"Processed {actions.Count} items in {sw.ElapsedMilliseconds}ms", 1, 1); return actions; } diff --git a/uSync.BackOffice/Services/SyncService_Single.cs b/uSync.BackOffice/Services/SyncService_Single.cs index f9b0b341b..6f0188384 100644 --- a/uSync.BackOffice/Services/SyncService_Single.cs +++ b/uSync.BackOffice/Services/SyncService_Single.cs @@ -59,8 +59,9 @@ public async Task> ReportPartialAsync(IList> ImportPartialAsync(IList> ImportPartialSecondPassAsync(IEnumer continue; } - options.Callbacks?.Update?.Invoke($"Second Pass: {action.Name}", - CalculateProgress(index, total, options.ProgressMin, options.ProgressMax), 100); + if (options.Callbacks is not null) + await options.Callbacks.RaiseUpdateAsync($"Second Pass: {action.Name}", + CalculateProgress(index, total, options.ProgressMin, options.ProgressMax), 100); secondPassActions.AddRange(await handlerPair.Handler.ImportSecondPassAsync(action, handlerPair.Settings, options)); @@ -273,7 +276,8 @@ public async Task> ImportPartialPostImportAsync(IEnumer { if (handlerPair.Handler is ISyncPostImportHandler postImportHandler) { - options.Callbacks?.Update?.Invoke(actionItem.alias, index, folders.Count); + if (options.Callbacks is not null) + await options.Callbacks.RaiseUpdateAsync(actionItem.alias, index, folders.Count); var handlerActions = actions.Where(x => x.HandlerAlias.InvariantEquals(handlerPair.Handler.Alias)); results.AddRange(await postImportHandler.ProcessPostImportAsync(handlerActions, handlerPair.Settings)); @@ -326,7 +330,8 @@ public async Task> ImportPostCleanFilesAsync(IEnumerabl if (handlerPair.Handler is ISyncCleanEntryHandler cleanEntryHandler) { - options.Callbacks?.Update?.Invoke(actionItem.alias, index, cleans.Count); + if (options.Callbacks is not null) + await options.Callbacks.RaiseUpdateAsync(actionItem.alias, index, cleans.Count); var handlerActions = actions.Where(x => x.HandlerAlias.InvariantEquals(handlerPair.Handler.Alias)); results.AddRange(await cleanEntryHandler.ProcessCleanActionsAsync(actionItem.folder, handlerActions, handlerPair.Settings)); diff --git a/uSync.BackOffice/SyncHandlers/SyncHandlerRoot.cs b/uSync.BackOffice/SyncHandlers/SyncHandlerRoot.cs index d5b3f2758..9be83d048 100644 --- a/uSync.BackOffice/SyncHandlers/SyncHandlerRoot.cs +++ b/uSync.BackOffice/SyncHandlers/SyncHandlerRoot.cs @@ -266,11 +266,13 @@ public async Task> ImportAllAsync(string[] folders, Han var cacheKey = PrepCaches(); try { - options.Callbacks?.Update?.Invoke("Calculating import order", 1, 9); + if (options.Callbacks is not null) + await options.Callbacks.RaiseUpdateAsync("Calculating import order", 1, 9); var items = await GetMergedItemsAsync(folders, new SyncMergeOptions(options.Callbacks?.Update)); - options.Callbacks?.Update?.Invoke($"Processing {items.Count} items", 2, 9); + if (options.Callbacks is not null) + await options.Callbacks.RaiseUpdateAsync($"Processing {items.Count} items", 2, 9); // create the update list with items.count space. this is the max size we need this list. List actions = new(items.Count); @@ -280,7 +282,8 @@ public async Task> ImportAllAsync(string[] folders, Han int count = 0; int total = items.Count; - options.Callbacks?.SetRange?.Invoke(count, total); + if (options.Callbacks is not null) + await options.Callbacks.RaiseSetRangeAsync(count, total); foreach (var item in items) { @@ -326,7 +329,8 @@ public async Task> ImportAllAsync(string[] folders, Han await PerformImportCleanAsync(cleanMarkers, actions, config, options.Callbacks?.Update); } - options.Callbacks?.Update?.Invoke("Done", 3, 3); + if (options.Callbacks is not null) + await options.Callbacks.RaiseUpdateAsync("Done", 3, 3); if (logger.IsEnabled(LogLevel.Debug)) logger.LogDebug("ImportAll: {count} items imported", actions.Count); @@ -458,7 +462,8 @@ virtual public async Task> ImportElementAsync(XElement { var actions = new List(); var elements = node.Elements().ToList(); - options.Callbacks?.SetRange?.Invoke(0, elements.Count); + if (options.Callbacks is not null) + await options.Callbacks.RaiseSetRangeAsync(0, elements.Count); foreach (var item in elements) { actions.AddRange(await ImportSingleElementAsync(new XElement(item), filename, settings, options)); @@ -492,7 +497,8 @@ virtual protected async Task> ImportSingleElementAsync( try { - options.Callbacks?.IncrementalUpdate?.Invoke(node.GetAlias()); + if (options.Callbacks is not null) + await options.Callbacks.RaiseIncrementalUpdateAsync(node.GetAlias()); // merge the options from the handler and any import options into our serializer options. var serializerOptions = new SyncSerializerOptions(options.Flags, settings.Settings, options.UserId); diff --git a/uSync.Backoffice.Management.Api/Services/uSyncManagementService.cs b/uSync.Backoffice.Management.Api/Services/uSyncManagementService.cs index 9ba186b7e..c63dd893d 100644 --- a/uSync.Backoffice.Management.Api/Services/uSyncManagementService.cs +++ b/uSync.Backoffice.Management.Api/Services/uSyncManagementService.cs @@ -269,7 +269,7 @@ await _syncActionService.StartProcessAsync(new SyncStartActionRequest if (actionRequest.StepNumber >= handlers.Count) { var actions = await PerformFinalSteps(requestId, action, handlerOptions, callbacks, user?.Username); - return SummerizeCompleteProcess(actionRequest, action, handlers, requestId, callbacks, actions); + return await SummerizeCompleteProcess(actionRequest, action, handlers, requestId, callbacks, actions); } @@ -282,7 +282,7 @@ await _syncActionService.StartProcessAsync(new SyncStartActionRequest var allActions = _syncManagementCache.GetCachedActions(requestId); var summaries = GetSummaries(action, handlers, actionRequest.StepNumber, allActions); - callbacks.Callback?.Invoke(new SyncProgressSummary(summaries, "Processing " + action.ToString(), handlers.Count)); + await callbacks.RaiseCallbackAsync(new SyncProgressSummary(summaries, "Processing " + action.ToString(), handlers.Count)); return new PerformActionResponse { @@ -293,15 +293,18 @@ await _syncActionService.StartProcessAsync(new SyncStartActionRequest }; } - private static PerformActionResponse SummerizeCompleteProcess(PerformActionRequest actionRequest, HandlerActions action, List handlers, Guid requestId, uSyncCallbacks callbacks, List actions) + private static async Task SummerizeCompleteProcess(PerformActionRequest actionRequest, HandlerActions action, List handlers, Guid requestId, uSyncCallbacks callbacks, List actions) { var finalSummary = GetSummaries(action, handlers, actionRequest.StepNumber + 1, actions); var actionViews = actions.Select(x => x.AsActionView()); - callbacks?.Callback?.Invoke(new SyncProgressSummary(finalSummary, "Completed", handlers.Count)); - callbacks?.Complete?.Invoke(requestId, "Sync complete", true, actionViews); + if (callbacks is not null) + { + await callbacks.RaiseCallbackAsync(new SyncProgressSummary(finalSummary, "Completed", handlers.Count)); + await callbacks.RaiseCompleteAsync(requestId, "Sync complete", true, actionViews); + } - // finished. + // finished. return new PerformActionResponse { RequestId = requestId.ToString(),