refactor(net): own live session composition

Extract reset, selection, entered-world, and route construction behind LiveSessionHost while preserving the sole LiveSessionController authority. Retain partial route and subscription cleanup for retry, and replace the embedded ACE-only shortcut with the exact named-retail unsigned skill formula.

Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-22 10:36:06 +02:00
parent 18d4b999de
commit 557eb7ef6b
22 changed files with 1430 additions and 236 deletions

View file

@ -7,36 +7,48 @@ namespace AcDream.Core.Net;
/// </summary>
internal sealed class SubscriptionSet : IDisposable
{
private List<IDisposable>? _subscriptions = [];
private readonly object _gate = new();
private readonly List<RetryableSubscription> _subscriptions = [];
private bool _disposeRequested;
public void Add(IDisposable subscription)
{
ArgumentNullException.ThrowIfNull(subscription);
List<IDisposable>? subscriptions = _subscriptions;
if (subscriptions is null)
{
subscription.Dispose();
throw new ObjectDisposedException(nameof(SubscriptionSet));
}
subscriptions.Add(subscription);
AddRetained(new RetryableSubscription(subscription.Dispose));
}
public void Add(Action unsubscribe)
{
ArgumentNullException.ThrowIfNull(unsubscribe);
Add(new ActionSubscription(unsubscribe));
AddRetained(new RetryableSubscription(unsubscribe));
}
private void AddRetained(RetryableSubscription retained)
{
bool disposeNow;
lock (_gate)
{
disposeNow = _disposeRequested;
_subscriptions.Add(retained);
}
if (!disposeNow)
return;
retained.Dispose();
throw new ObjectDisposedException(nameof(SubscriptionSet));
}
public void Dispose()
{
List<IDisposable>? subscriptions =
Interlocked.Exchange(ref _subscriptions, null);
if (subscriptions is null)
return;
RetryableSubscription[] subscriptions;
lock (_gate)
{
_disposeRequested = true;
subscriptions = _subscriptions.ToArray();
}
List<Exception>? errors = null;
for (int i = subscriptions.Count - 1; i >= 0; i--)
for (int i = subscriptions.Length - 1; i >= 0; i--)
{
try
{
@ -52,10 +64,59 @@ internal sealed class SubscriptionSet : IDisposable
throw new AggregateException("one or more subscriptions failed to detach", errors);
}
private sealed class ActionSubscription(Action unsubscribe) : IDisposable
private sealed class RetryableSubscription(Action dispose) : IDisposable
{
private Action? _unsubscribe = unsubscribe;
private readonly object _gate = new();
private Action? _dispose = dispose;
private bool _executing;
private int _executingThreadId;
public void Dispose() => Interlocked.Exchange(ref _unsubscribe, null)?.Invoke();
public void Dispose()
{
Action? operation;
int threadId = Environment.CurrentManagedThreadId;
lock (_gate)
{
while (_executing)
{
if (_executingThreadId == threadId)
{
throw new InvalidOperationException(
"Subscription cleanup cannot complete reentrantly.");
}
Monitor.Wait(_gate);
}
operation = _dispose;
if (operation is null)
return;
_executing = true;
_executingThreadId = threadId;
}
try
{
operation();
}
catch
{
CompleteAttempt(succeeded: false);
throw;
}
CompleteAttempt(succeeded: true);
}
private void CompleteAttempt(bool succeeded)
{
lock (_gate)
{
if (succeeded)
_dispose = null;
_executing = false;
_executingThreadId = 0;
Monitor.PulseAll(_gate);
}
}
}
}