// // NosThreadSynchronizer.cs // // Copyright (c) František Boháček. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.Collections.Concurrent; using System.Net; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using NosSmooth.LocalBinding.Objects; using NosSmooth.LocalBinding.Options; using Remora.Results; namespace NosSmooth.LocalBinding; /// /// Synchronizes with NosTale thread using a periodic function. /// public class NosThreadSynchronizer { private readonly PeriodicBinding _periodicBinding; private readonly NosThreadSynchronizerOptions _options; private readonly ConcurrentQueue _queuedOperations; /// /// Initializes a new instance of the class. /// /// The periodic function binding. /// The options. public NosThreadSynchronizer(PeriodicBinding periodicBinding, IOptions options) { _periodicBinding = periodicBinding; _options = options.Value; _queuedOperations = new ConcurrentQueue(); } /// /// Start the synchronizer operation. /// public void StartSynchronizer() { _periodicBinding.Periodic += Periodic; } /// /// Stop the synchronizer operation. /// public void StopSynchronizer() { _periodicBinding.Periodic -= Periodic; } private void Periodic() { var tasks = _options.MaxTasksPerIteration; while (tasks-- > 0 && _queuedOperations.TryDequeue(out var operation)) { ExecuteOperation(operation); } } private void ExecuteOperation(SyncOperation operation) { try { var result = operation.action(); operation.Result = result; } catch (Exception e) { // TODO: log? operation.Result = e; } if (operation.CancellationTokenSource is not null) { try { operation.CancellationTokenSource.Cancel(); } catch (Exception) { // ignore } } } /// /// Enqueue the given operation to execute on next frame. /// /// The action to execute. public void EnqueueOperation(Action action) { _queuedOperations.Enqueue ( new SyncOperation ( () => { action(); return Result.FromSuccess(); }, null ) ); } /// /// Synchronizes to NosTale thread, executes the given action and returns its result. /// /// The action to execute. /// The cancellation token used for cancelling the operation. /// The result of the action. public async Task SynchronizeAsync(Func action, CancellationToken ct = default) { var linkedSource = CancellationTokenSource.CreateLinkedTokenSource(ct); var syncOperation = new SyncOperation(action, linkedSource); _queuedOperations.Enqueue(syncOperation); try { await Task.Delay(Timeout.Infinite, linkedSource.Token); } catch (OperationCanceledException) { if (ct.IsCancellationRequested) { // Throw in case the top token was cancelled. throw; } } catch (Exception e) { return new ExceptionError(e); } return syncOperation.Result ?? Result.FromSuccess(); } private record SyncOperation(Func action, CancellationTokenSource? CancellationTokenSource) { public Result? Result { get; set; } } }