using System; using System.Text.Json; using System.Threading; using System.Threading.Tasks; namespace Acp; /// /// Shared plumbing for the two ACP role connections: owns the , runs /// its read loop, and provides typed send/handle helpers that (de)serialize ACP message records. /// public abstract class AcpConnection : IDisposable { private protected JsonRpcEndpoint Endpoint { get; } private IAcpTransport Transport { get; } private CancellationTokenSource Cts { get; } = new(); private Task? ReadLoop { get; set; } private protected AcpConnection(IAcpTransport transport) { Transport = transport; Endpoint = new JsonRpcEndpoint(transport); } /// /// The background read loop, faulting only on an unexpected failure. The owner can observe this /// to learn the connection has died (a clean stream close or cancellation completes it). Throws /// if has not been called. /// public Task Completion => ReadLoop ?? throw new InvalidOperationException("Start has not been called."); /// /// Starts the background read loop. Call once after construction; a second call is a no-op so we /// never spawn a duplicate loop on the same transport. /// public void Start() => ReadLoop ??= Task.Run(() => Endpoint.RunAsync(Cts.Token)); private protected async ValueTask RequestAsync(string method, TRequest request, CancellationToken ct) { JsonElement @params = JsonSerializer.SerializeToElement(request, AcpJson.Options); JsonRpcResponse response = await Endpoint.SendRequestAsync(method, @params, ct).ConfigureAwait(false); if (response.Error is not null) throw new AcpException(response.Error); if (response.Result is not { } result) throw new AcpException($"Response to '{method}' had neither result nor error."); return result.Deserialize(AcpJson.Options)!; } private protected ValueTask NotifyAsync(string method, TNotification notification, CancellationToken ct) => Endpoint.SendNotificationAsync(method, JsonSerializer.SerializeToElement(notification, AcpJson.Options), ct); private protected async ValueTask ExtRequestAsync(string method, JsonElement @params, CancellationToken ct) { JsonRpcResponse response = await Endpoint.SendRequestAsync(method, @params, ct).ConfigureAwait(false); if (response.Error is not null) throw new AcpException(response.Error); return response.Result ?? default; } private protected void HandleRequest(string method, Func> impl) => Endpoint.OnRequest(method, async (request, ct) => { if (request.Params is not { } pe) throw new AcpException($"Missing params for '{method}'", (int)JsonRpcErrorCode.InvalidParams); TRequest parsed = pe.Deserialize(AcpJson.Options)!; TResponse result = await impl(parsed, ct).ConfigureAwait(false); return new JsonRpcResponse { Id = request.Id, Result = JsonSerializer.SerializeToElement(result, AcpJson.Options) }; }); private protected void HandleNotification(string method, Func impl) => Endpoint.OnNotification(method, async (notification, ct) => { if (notification.Params is not { } pe) throw new AcpException($"Missing params for notification '{method}'", (int)JsonRpcErrorCode.InvalidParams); TNotification parsed = pe.Deserialize(AcpJson.Options)!; await impl(parsed, ct).ConfigureAwait(false); }); public void Dispose() { Cts.Cancel(); Transport.Dispose(); Cts.Dispose(); GC.SuppressFinalize(this); } }