//
// 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;
}
}