// // DefaultContract.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; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Remora.Results; namespace NosSmooth.Core.Contracts; /// /// A generic implementation of contract /// supporting any data. /// /// The data type. /// The states. /// The errors that may be returned. public class DefaultContract : IContract where TState : struct, IComparable where TData : notnull { /// /// An action to execute when a state is reached. /// /// The data that led to the state. /// The cancellation token used for cancelling the operation. /// The result that may or may not have succeeded. public delegate Task> StateActionAsync(object? data, CancellationToken ct); /// /// An action to execute when a state that may fill the data is reached. /// Returns the data to fill. /// /// The data that led to the state. /// The cancellation token used for cancelling the operation. /// The result that may or may not have succeeded. public delegate Task> FillDataAsync(object data, CancellationToken ct); private readonly IDictionary _actions; private readonly Contractor _contractor; private readonly TState _defaultState; private readonly TState _fillAtState; private readonly FillDataAsync _fillData; private readonly TimeSpan? _timeout; private TError? _error; private Result? _resultError; private TState? _waitingFor; private bool _unregisterAtWaitingFor; private CancellationTokenSource? _waitCancellationSource; /// /// Initializes a new instance of the class. /// /// The contractor. /// The default state. /// The state to fill data at. /// The function to fill the data. /// The actions to execute at each state. /// The timeout. public DefaultContract ( Contractor contractor, TState defaultState, TState fillAtState, FillDataAsync fillData, IDictionary actions, TimeSpan? timeout ) { _timeout = timeout; _defaultState = defaultState; _contractor = contractor; CurrentState = defaultState; _actions = actions; _fillData = fillData; _fillAtState = fillAtState; } /// public TState CurrentState { get; private set; } /// public TData? Data { get; private set; } /// public bool IsRegistered { get; private set; } /// public void Register() { if (!IsRegistered) { _contractor.Register(this); IsRegistered = true; } } /// public void Unregister() { if (IsRegistered) { _contractor.Unregister(this); IsRegistered = false; } } /// public async Task> Update(TAny data, CancellationToken ct = default) { if (!_actions.ContainsKey(CurrentState)) { throw new Exception(); // ? } var result = await _actions[CurrentState](data, ct); if (!result.IsDefined(out var resultData)) { _resultError = Result.FromError(result); _waitCancellationSource?.Cancel(); return Result.FromError(result); } if (resultData.Error is not null) { _error = resultData.Error; _waitCancellationSource?.Cancel(); } if (resultData.NextState is null) { return ContractUpdateResponse.NotInterested; } CurrentState = resultData.NextState.Value; if (_fillAtState.CompareTo(CurrentState) == 0) { var filledResult = await _fillData(data!, ct); if (!filledResult.IsDefined(out var filled)) { _resultError = Result.FromError(filledResult); _waitCancellationSource?.Cancel(); return Result.FromError(filledResult); } Data = filled; } if (_waitingFor is not null && _waitingFor.Value.CompareTo(CurrentState) == 0) { IsRegistered = false; // avoid deadlock. The cancellation will trigger unregister, // but we are inside of the lock now. _waitCancellationSource?.Cancel(); if (_unregisterAtWaitingFor) { return ContractUpdateResponse.InterestedAndUnregister; } } // TODO: timeouts! return ContractUpdateResponse.Interested; } /// public async Task OnlyExecuteAsync(CancellationToken ct = default) { if (_actions.ContainsKey(_defaultState)) { var result = await _actions[_defaultState](default, ct); if (!result.IsSuccess) { return Result.FromError(result); } } return Result.FromSuccess(); } /// public async Task> WaitForAsync (TState state, bool unregisterAfter = true, CancellationToken ct = default) { if (_fillAtState.CompareTo(state) > 0) { throw new InvalidOperationException ( $"The requested state {state} does not guarantee data filled. The state that fills data is {_defaultState}" ); } _waitingFor = state; _unregisterAtWaitingFor = unregisterAfter; _waitCancellationSource = CancellationTokenSource.CreateLinkedTokenSource(ct); if (_timeout is not null) { _waitCancellationSource.CancelAfter(_timeout.Value); } Register(); if (CurrentState.CompareTo(_defaultState) == 0) { var result = await OnlyExecuteAsync(ct); if (!result.IsSuccess) { Unregister(); return Result.FromError(result); } } try { await Task.Delay(-1, _waitCancellationSource.Token); } catch { // ignored } finally { if (unregisterAfter) { Unregister(); } } if (ct.IsCancellationRequested) { throw new TaskCanceledException(); } if (_resultError is not null) { return Result.FromError(_resultError.Value); } if (_error is not null) { return new ContractError(_error); } if (Data is null) { throw new Exception("Data was null, but shouldn't have. There is an error in DefaultContract."); } return Data; } }