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);
}
}