Internals/Runtimes/Runtime.cs
using System.Runtime.CompilerServices;

namespace Sandbox.Reactivity.Internals.Runtimes;

internal sealed class Runtime : IDisposable
{
	/// <summary>
	/// Effects that are waiting to run due to reactivity changes.
	/// </summary>
	private readonly Queue<Effect> _pendingEffects = new(16);

	/// <summary>
	/// How many effects have run during a flush operation.
	/// </summary>
	private uint _flushDepth;

	/// <summary>
	/// Whether pending effects are currently being run.
	/// </summary>
	private bool _isFlushing;

	/// <summary>
	/// The currently executing effect.
	/// </summary>
	public Effect? CurrentEffect { get; set; }

	/// <summary>
	/// The currently executing reaction.
	/// </summary>
	public IReaction? CurrentReaction { get; set; }

	/// <summary>
	/// A monotonically increasing counter that's incremented when an <see cref="IProducer" /> updates its current
	/// value.
	/// </summary>
	public uint Version { get; set; } = 1;

	/// <summary>
	/// Whether dependency tracking is currently disabled.
	/// </summary>
	public bool IsUntracking { get; set; }

	/// <summary>
	/// Whether a flush was scheduled to run at the end of the frame.
	/// </summary>
	public bool IsFlushScheduled { get; private set; }

	/// <summary>
	/// Whether an effect is currently executing its teardown function. This is used by producers to skip any
	/// recomputation when accessed to ensure the previous value is returned.
	/// </summary>
	public bool IsRunningTeardown { get; set; }

	public void Dispose()
	{
		_pendingEffects.Clear();
		CurrentEffect = null;
		CurrentReaction = null;
		Version = uint.MaxValue;
		IsUntracking = true;
		IsFlushScheduled = false;
		_isFlushing = false;
	}

	/// <summary>
	/// Returns the currently executing effect.
	/// </summary>
	/// <exception cref="InvalidOperationException">
	/// Thrown if there is no effect that is currently executing.
	/// </exception>
	public Effect EnsureCurrentEffect([CallerMemberName] string name = "Effect")
	{
		return CurrentEffect ?? throw new InvalidOperationException(name + " must be created in an effect root");
	}

	public void ScheduleEffect(Effect effect)
	{
		_pendingEffects.Enqueue(effect);

		if (!IsFlushScheduled && !_isFlushing)
		{
			IsFlushScheduled = true;
		}
	}

	/// <summary>
	/// Empties the queue of effects that are scheduled to run due to one of their dependencies changing. This should
	/// only be run when you want an effect to re-run immediately after changing a reactive value.
	/// </summary>
	public void Flush()
	{
		if (_isFlushing)
		{
			return;
		}

		_isFlushing = true;
		IsFlushScheduled = false;

		try
		{
			while (_pendingEffects.TryDequeue(out var effect))
			{
				if (_flushDepth++ > 1000)
				{
					_pendingEffects.Clear();
					_pendingEffects.TrimExcess(16);

#if DEBUG
#else
					throw new InfiniteLoopException();
#endif
				}

				if (effect.ShouldRun)
				{
					effect.Run();
#if DEBUG
#endif
				}
			}
		}
		finally
		{
			_flushDepth = 0;
			_isFlushing = false;
#if DEBUG
#endif
		}
	}

#if DEBUG
#endif
}