Files

70 lines
2.3 KiB
C#
Raw Permalink Normal View History

2026-08-04 02:46:53 -07:00
namespace NucleicApp.Services;
public enum OnboardingPhase
{
Idle,
StartingHost,
Connecting,
Ready,
Failed,
}
/// First-run coordinator for the item-11 core. Component installation and OAuth screens plug
/// into the same phase surface once their host intents land; host launch + authenticated local
/// sync are real today.
public sealed class OnboardingService
{
private readonly HostdLauncher launcher;
private readonly HostConnection connection;
public OnboardingPhase Phase { get; private set; } = OnboardingPhase.Idle;
public string? Failure { get; private set; }
public event Action<OnboardingPhase>? PhaseChanged;
public OnboardingService(HostdLauncher launcher, HostConnection connection)
{
this.launcher = launcher;
this.connection = connection;
}
public async Task StartAsync(string rendezvousPath, CancellationToken cancellationToken = default)
{
try
{
SetPhase(OnboardingPhase.StartingHost);
_ = await launcher.EnsureRunningAsync(cancellationToken).ConfigureAwait(false);
SetPhase(OnboardingPhase.Connecting);
var ready = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
void Changed(HostConnectionState state)
{
if (state == HostConnectionState.Connected) ready.TrySetResult();
else if (state == HostConnectionState.Failed)
ready.TrySetException(new InvalidOperationException(
connection.LastProtocolState?.Message ?? "local host connection failed"));
}
connection.StateChanged += Changed;
try
{
await connection.StartLocalAsync(rendezvousPath, cancellationToken).ConfigureAwait(false);
await ready.Task.WaitAsync(TimeSpan.FromSeconds(30), cancellationToken).ConfigureAwait(false);
}
finally { connection.StateChanged -= Changed; }
SetPhase(OnboardingPhase.Ready);
}
catch (Exception error)
{
Failure = error.Message;
SetPhase(OnboardingPhase.Failed);
throw;
}
}
private void SetPhase(OnboardingPhase phase)
{
if (Phase == phase) return;
Phase = phase;
PhaseChanged?.Invoke(phase);
}
}