acdream/src/AcDream.App/Composition/AnimationHookRegistrationSet.cs

151 lines
4.4 KiB
C#

using AcDream.App.Rendering;
using AcDream.Core.Physics;
namespace AcDream.App.Composition;
/// <summary>
/// Lifetime owner for animation-hook router edges. Registration order remains
/// retail-observable dispatch order; teardown runs in reverse and retains only
/// unregister operations that actually failed.
/// </summary>
internal sealed class AnimationHookRegistrationSet : IDisposable,
IRetryableResourceCleanup
{
private sealed class Entry(IAnimationHookSink sink)
{
public IAnimationHookSink Sink { get; } = sink;
public bool Removed { get; set; }
}
private readonly Action<IAnimationHookSink> _register;
private readonly Action<IAnimationHookSink> _unregister;
private readonly List<Entry> _entries = [];
private bool _disposing;
private bool _closed;
public AnimationHookRegistrationSet(AnimationHookRouter router)
: this(
(router ?? throw new ArgumentNullException(nameof(router))).Register,
router.Unregister)
{
}
internal AnimationHookRegistrationSet(
Action<IAnimationHookSink> register,
Action<IAnimationHookSink> unregister)
{
_register = register ?? throw new ArgumentNullException(nameof(register));
_unregister = unregister ?? throw new ArgumentNullException(nameof(unregister));
}
public bool IsCleanupComplete =>
_entries.All(static entry => entry.Removed);
public int ActiveCount =>
_entries.Count(static entry => !entry.Removed);
public void Register(IAnimationHookSink sink)
{
ArgumentNullException.ThrowIfNull(sink);
ObjectDisposedException.ThrowIf(_closed, this);
if (_entries.Any(entry =>
!entry.Removed && ReferenceEquals(entry.Sink, sink)))
{
return;
}
_register(sink);
_entries.Add(new Entry(sink));
}
public IDisposable RegisterOwned(IAnimationHookSink sink)
{
ArgumentNullException.ThrowIfNull(sink);
ObjectDisposedException.ThrowIf(_closed, this);
if (_entries.Any(entry =>
!entry.Removed && ReferenceEquals(entry.Sink, sink)))
{
throw new InvalidOperationException(
"The animation-hook sink is already registered and cannot be adopted twice.");
}
_register(sink);
_entries.Add(new Entry(sink));
return new Binding(this, sink);
}
private void UnregisterOwned(IAnimationHookSink sink)
{
Entry? entry = _entries.LastOrDefault(candidate =>
!candidate.Removed && ReferenceEquals(candidate.Sink, sink));
if (entry is null)
return;
_unregister(entry.Sink);
entry.Removed = true;
}
public void Dispose()
{
_closed = true;
RetryCleanup();
}
public void RetryCleanup()
{
if (_disposing || IsCleanupComplete)
return;
_closed = true;
_disposing = true;
List<Exception>? failures = null;
try
{
for (int i = _entries.Count - 1; i >= 0; i--)
{
Entry entry = _entries[i];
if (entry.Removed)
continue;
try
{
_unregister(entry.Sink);
entry.Removed = true;
}
catch (Exception failure)
{
(failures ??= []).Add(new InvalidOperationException(
$"Animation-hook sink '{entry.Sink.GetType().Name}' could not be unregistered.",
failure));
}
}
}
finally
{
_disposing = false;
}
if (failures is not null)
throw new AggregateException(
"Animation-hook registration cleanup remains incomplete.",
failures);
}
private sealed class Binding : IDisposable
{
private AnimationHookRegistrationSet? _owner;
private readonly IAnimationHookSink _sink;
public Binding(AnimationHookRegistrationSet owner, IAnimationHookSink sink)
{
_owner = owner;
_sink = sink;
}
public void Dispose()
{
AnimationHookRegistrationSet? owner = _owner;
if (owner is null)
return;
owner.UnregisterOwned(_sink);
_owner = null;
}
}
}