using System.Collections.Concurrent;
using ClaudeDo.Worker.Hub;
namespace ClaudeDo.Worker.Lifecycle;
///
/// In-memory registry of Worker operations running right now (verify, merge, rebase, worktree
/// creation). registers and broadcasts; disposing the returned scope removes
/// the entry, so a failed or thrown operation is always cleaned up by its caller's using/finally.
///
public sealed class RunningOperationRegistry
{
private readonly ConcurrentDictionary _active = new();
private readonly HubBroadcaster? _broadcaster;
public RunningOperationRegistry(HubBroadcaster? broadcaster = null) => _broadcaster = broadcaster;
/// The operations running right now, oldest first.
public IReadOnlyList Snapshot() =>
_active.Values.OrderBy(o => o.StartedAt).ToList();
/// Registers an operation; dispose the result when it ends (idempotent).
public IDisposable Begin(string kind, string title, string? taskId = null, int? taskNumber = null, string? listId = null)
{
var op = new RunningOperationDto(Guid.NewGuid().ToString("N"), kind, taskId, taskNumber, listId, title, DateTime.UtcNow);
_active[op.Id] = op;
_ = Broadcast(_broadcaster?.OperationStarted(op));
return new Scope(this, op.Id);
}
// A broadcast failure must never fail the operation it describes.
private static async Task Broadcast(Task? send)
{
if (send is null) return;
try { await send; } catch { /* no clients / hub down */ }
}
private sealed class Scope(RunningOperationRegistry owner, string id) : IDisposable
{
private int _disposed;
public void Dispose()
{
if (Interlocked.Exchange(ref _disposed, 1) != 0) return;
owner._active.TryRemove(id, out _);
_ = Broadcast(owner._broadcaster?.OperationEnded(id));
}
}
}