Move the canonical WorldSession generation, connect/enter/tick/stop transaction, inbound subscription owner, and retryable teardown acknowledgements into AcDream.Runtime. Keep App as a borrowing graphical host with a single inertable command projection and no mirrored session state. Validated by 79 Runtime tests, 3,776 App tests with three existing skips, the Release solution build, and 8,428 complete Release tests with five existing skips. Co-authored-by: Codex <codex@openai.com>
102 lines
2.9 KiB
C#
102 lines
2.9 KiB
C#
using AcDream.Core.Net;
|
|
using AcDream.Runtime.Session;
|
|
using AcDream.UI.Abstractions;
|
|
|
|
namespace AcDream.App.Net;
|
|
|
|
/// <summary>
|
|
/// Borrowed graphical view over the canonical Runtime live-session owner.
|
|
/// This adapter stores no generation, transport, or in-world state.
|
|
/// </summary>
|
|
internal sealed class LiveSessionAppSource
|
|
: ILiveInWorldSource,
|
|
ILiveWorldSessionSource,
|
|
ILiveUiSessionTarget
|
|
{
|
|
private readonly LiveSessionController _session;
|
|
private readonly LiveSessionCommandSurface _commands;
|
|
|
|
public LiveSessionAppSource(
|
|
LiveSessionController session,
|
|
LiveSessionCommandSurface commands)
|
|
{
|
|
_session = session ?? throw new ArgumentNullException(nameof(session));
|
|
_commands = commands ?? throw new ArgumentNullException(nameof(commands));
|
|
}
|
|
|
|
public bool IsInWorld => _session.IsInWorld;
|
|
|
|
public WorldSession? CurrentSession => _session.CurrentSession;
|
|
|
|
public ICommandBus Commands => _commands;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Stable App command projection over one borrowed generation route. The
|
|
/// retained UI may keep this surface, while the displaced route itself becomes
|
|
/// inert before inbound subscriptions detach.
|
|
/// </summary>
|
|
internal sealed class LiveSessionCommandSurface : ICommandBus
|
|
{
|
|
private readonly object _gate = new();
|
|
private LiveSessionCommandRouter? _active;
|
|
|
|
public ILiveSessionCommandRouting Attach(LiveSessionCommandRouter route)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(route);
|
|
lock (_gate)
|
|
{
|
|
if (_active is not null)
|
|
{
|
|
throw new InvalidOperationException(
|
|
"A graphical live-session command route is already attached.");
|
|
}
|
|
|
|
_active = route;
|
|
return new RouteLease(this, route);
|
|
}
|
|
}
|
|
|
|
public void Publish<T>(T command) where T : notnull
|
|
{
|
|
LiveSessionCommandRouter? route;
|
|
lock (_gate)
|
|
route = _active;
|
|
route?.Publish(command);
|
|
}
|
|
|
|
private void Release(LiveSessionCommandRouter expected)
|
|
{
|
|
expected.Dispose();
|
|
lock (_gate)
|
|
{
|
|
if (ReferenceEquals(_active, expected))
|
|
_active = null;
|
|
}
|
|
}
|
|
|
|
private sealed class RouteLease(
|
|
LiveSessionCommandSurface owner,
|
|
LiveSessionCommandRouter route)
|
|
: ILiveSessionCommandRouting
|
|
{
|
|
private readonly object _gate = new();
|
|
private LiveSessionCommandSurface? _owner = owner;
|
|
|
|
public void Activate() => route.Activate();
|
|
|
|
public void Dispose()
|
|
{
|
|
lock (_gate)
|
|
{
|
|
if (_owner is null)
|
|
return;
|
|
|
|
// Retain the owner until the route has physically become
|
|
// inert so a failed teardown remains retryable.
|
|
_owner.Release(route);
|
|
_owner = null;
|
|
}
|
|
}
|
|
}
|
|
}
|