using System.Diagnostics.CodeAnalysis; using Grpc.Core; using Grpc.Net.Client; using Mishima.Nexus.Wire.V1; using Robotico.Graph; using Robotico.Nexus.Client.Document; using Robotico.Nexus.Client.Graph; using Robotico.Nexus.Client.Relational; using Robotico.Nexus.Client.Time; using Robotico.Result.Errors; using Robotico.Time; namespace Robotico.Nexus.Client; /// /// Connects to a Nexus wire endpoint and exposes plane facades: / /// implement Robotico.Graph / Robotico.Time ports over gRPC, and /// / return thin document/relational /// façades (MVP — no full IMishimaDatabase façade; use / /// for the full generated Wire.V1 RPC surface). /// One connection is bound to a single scheme + logical database from the connection URI; open a /// second to talk to another plane or database. /// public sealed class NexusClient : IAsyncDisposable, IDisposable { private readonly GrpcChannel _channel; private readonly string? _bearerToken; private bool _disposed; private NexusClient(GrpcChannel channel, NexusConnectionUriParts parsed, string boundDatabase, string? bearerToken) { _channel = channel; _bearerToken = bearerToken; Scheme = parsed.Scheme; Database = boundDatabase; Graph = new NexusGraphStore(channel, bearerToken, boundDatabase); Time = new NexusTimeStore(channel, bearerToken, boundDatabase); } /// Connection scheme () resolved at connect time. public string Scheme { get; } /// Logical database bound at connect time (URI path segment, or the scheme's canonical default). public string Database { get; } /// /// Graph plane store bound to when is /// ; usable against any logical database name via /// regardless of scheme. /// public IGraphStore Graph { get; } /// /// Time plane store bound to when is /// ; usable against any logical database name via /// regardless of scheme. /// public ITimeStore Time { get; } /// Connects to a Nexus wire endpoint. Validates the URI and opens a gRPC channel eagerly. [SuppressMessage("Design", "CA1054:URI-like parameters should not be strings", Justification = "Nexus connection strings use custom nexus+* schemes and are the documented public API surface (mirrors ADO.NET / MongoDB connection-string ergonomics); a System.Uri overload adds no value here.")] [SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "Ownership of the returned NexusClient (and its channel) transfers to the caller via Result.Success; the caller is responsible for disposing it.")] public static Robotico.Result.Result Connect(string connectionUri, NexusClientOptions? options = null) { Robotico.Result.Result parsedResult = NexusConnectionUri.TryParse(connectionUri); if (!parsedResult.IsSuccess(out NexusConnectionUriParts? parsed, out IError? parseError)) { return Robotico.Result.Result.Error(parseError!); } options ??= new NexusClientOptions(); NexusTlsMaterial? tls = options.ResolveTls(); bool useHttps = options.ResolveUseHttps(tls); Robotico.Result.Result channelResult = NexusGrpcChannelFactory.CreateChannel(parsed!, tls, useHttps); if (!channelResult.IsSuccess(out GrpcChannel? channel, out IError? channelError)) { return Robotico.Result.Result.Error(channelError!); } string database = string.IsNullOrWhiteSpace(parsed!.Database) ? NexusConnectionUri.DefaultDatabase(parsed.Scheme) : parsed.Database!; return Robotico.Result.Result.Success(new NexusClient(channel!, parsed, database, options.ResolveBearerToken())); } /// Calls NexusHealthService.Check and succeeds only when the server reports ready. public Robotico.Result.Result CheckHealth() { Robotico.Result.Result disposed = EnsureNotDisposed(); if (!disposed.IsSuccess()) { return disposed; } NexusHealthService.NexusHealthServiceClient client = new(_channel); try { HealthCheckResponse response = client.Check(new HealthCheckRequest(), NexusCallMetadata.Build(Database, _bearerToken)); return response.Ready ? Robotico.Result.Result.Success() : Robotico.Result.Result.Error(NexusErrors.Error("Nexus health check reports not ready.", NexusErrors.Unavailable)); } catch (RpcException ex) { return Robotico.Result.Result.Error(NexusWireStatus.FromException(ex)); } } /// Creates a thin document-plane façade (Ping + single-document upsert/get/delete) bound to this channel. public Robotico.Result.Result CreateDocumentClient() { Robotico.Result.Result disposed = EnsureNotDisposed(); return disposed.IsSuccess() ? Robotico.Result.Result.Success(new NexusDocumentClient(_channel, Database, _bearerToken)) : Robotico.Result.Result.Error(AsError(disposed)); } /// Creates a thin relational-plane façade (Ping only) bound to this channel. public Robotico.Result.Result CreateRelationalClient() { Robotico.Result.Result disposed = EnsureNotDisposed(); return disposed.IsSuccess() ? Robotico.Result.Result.Success(new NexusRelationalClient(_channel, Database, _bearerToken)) : Robotico.Result.Result.Error(AsError(disposed)); } /// Creates the generated document-plane Wire.V1 client bound to this channel (full RPC surface). public Robotico.Result.Result CreateDocumentWireClient() { Robotico.Result.Result disposed = EnsureNotDisposed(); return disposed.IsSuccess() ? Robotico.Result.Result.Success(new NexusDocumentEngineService.NexusDocumentEngineServiceClient(_channel)) : Robotico.Result.Result.Error(AsError(disposed)); } /// Creates the generated relational-plane Wire.V1 client bound to this channel (full RPC surface). public Robotico.Result.Result CreateRelationalWireClient() { Robotico.Result.Result disposed = EnsureNotDisposed(); return disposed.IsSuccess() ? Robotico.Result.Result.Success(new NexusRelationalEngineService.NexusRelationalEngineServiceClient(_channel)) : Robotico.Result.Result.Error(AsError(disposed)); } /// Metadata headers a caller can attach to a manual RPC (document/relational clients) for this database. public Metadata BuildCallMetadata(string? databaseNameOverride = null) => NexusCallMetadata.Build(string.IsNullOrWhiteSpace(databaseNameOverride) ? Database : databaseNameOverride, _bearerToken); /// public void Dispose() { if (_disposed) { return; } _disposed = true; _channel.Dispose(); } /// public ValueTask DisposeAsync() { if (_disposed) { return ValueTask.CompletedTask; } _disposed = true; _channel.Dispose(); return ValueTask.CompletedTask; } private Robotico.Result.Result EnsureNotDisposed() => _disposed ? Robotico.Result.Result.Error(NexusErrors.Error("NexusClient is disposed.", NexusErrors.Disposed)) : Robotico.Result.Result.Success(); private static IError AsError(Robotico.Result.Result failed) { failed.IsError(out IError? error); return error ?? NexusErrors.Error("NexusClient operation failed."); } }