Code/Internals/IReaction.cs
using Sandbox.Reactivity.Internals.Runtimes;
namespace Sandbox.Reactivity.Internals;
/// <summary>
/// An object that reacts to changes made by any <see cref="IProducer" /> added as dependencies.
/// </summary>
internal interface IReaction : IReactiveObject
{
/// <summary>
/// The list of <see cref="IProducer" />s that this reaction depends on. If any of the producers change their value,
/// this reaction will run.
/// </summary>
List<IProducer> Dependencies { get; }
/// <summary>
/// The <see cref="Runtime.Version" /> at which this reaction last ran. This is used to check if this reaction has
/// any dependencies out of date. Reactions updating their read version do not modify the global version.
/// </summary>
uint ReadVersion { get; }
/// <summary>
/// Whether this reaction might potentially need to re-run.
/// </summary>
ReactionState State { get; set; }
/// <summary>
/// Whether this reaction is an effect or is depended upon by an effect, either directly or through an ancestor.
/// Reactions can use this to avoid running if they won't cause any observable difference.
/// </summary>
bool IsConnectedToEffect { get; }
/// <summary>
/// Called when one of this reaction's dependencies (or ancestor dependencies) have changed.
/// </summary>
/// <param name="state">The new state that is being propagated by the dependency.</param>
void OnDependencyChanged(ReactionState state);
/// <summary>
/// Called when this reaction should run due to a direct dependency changing its value.
/// </summary>
void Run();
}
internal static class ReactionExtensions
{
extension(IReaction reaction)
{
/// <summary>
/// <para>
/// Whether this reaction should call its <see cref="IReaction.Run" /> method. The result is derived from its
/// current <see cref="IReaction.State" />, and whether any of its direct dependencies have changed their value.
/// </para>
/// <para>
/// Some reactions can also produce values (e.g. derived states), so they will be run if possible in order to
/// bring them up to date before checking if its value changed.
/// </para>
/// </summary>
internal bool ShouldRun
{
get
{
switch (reaction.State)
{
case ReactionState.Stale:
// definitely run if we're completely stale
return true;
case ReactionState.PossiblyStale:
{
// check if any direct dependencies changed their value
foreach (var producer in reaction.Dependencies)
{
// run any reactions to make sure their values are up to date
if (producer is IReaction { ShouldRun: true } producerReaction)
{
producerReaction.Run();
}
if (producer.WriteVersion > reaction.ReadVersion)
{
return true;
}
}
if (reaction.IsConnectedToEffect)
{
// one of our ancestor dependencies changed but didn't cause an observable difference to
// this reaction, so we're up to date
reaction.State = ReactionState.UpToDate;
}
// reactions that aren't used in any effects remove themselves as a reaction to their
// dependencies to avoid unnecessary runs. in this case, we always need to check dependencies
// in order to determine if we should run again
return false;
}
case ReactionState.UpToDate:
return false;
default:
throw new ArgumentOutOfRangeException();
}
}
}
/// <summary>
/// Adds a producer as a dependency to this reaction.
/// </summary>
/// <param name="producer">The producer to add as a dependency.</param>
internal void AddDependency(IProducer producer)
{
if (!reaction.Dependencies.Contains(producer))
{
reaction.Dependencies.Add(producer);
}
}
}
}