using Google.Protobuf.WellKnownTypes; using ZB.MOM.WW.MxGateway.Contracts.Proto; namespace ZB.MOM.WW.MxGateway.Server.Grpc; /// /// Maps between worker IPC types and gRPC contract types. /// public sealed class MxAccessGrpcMapper { private readonly TimeProvider _timeProvider; /// /// Initializes the mapper with an optional time provider. /// /// Time provider for timestamps; defaults to system time if null. public MxAccessGrpcMapper(TimeProvider? timeProvider = null) { _timeProvider = timeProvider ?? TimeProvider.System; } /// /// Maps a gRPC MX command request to a worker command. /// /// Request payload. /// The mapped ready for worker dispatch. public WorkerCommand MapCommand(MxCommandRequest request) { ArgumentNullException.ThrowIfNull(request); ArgumentNullException.ThrowIfNull(request.Command); return MapCommand(request.Command); } /// /// Maps a gRPC MX command to a worker command. Callers that already hold the command — including /// the constraint pipeline, whose rewritten command is not the one on the request — use this /// overload rather than cloning a whole request to carry a single field; nothing outside /// the command is read here. /// /// Command payload. /// The mapped ready for worker dispatch. public WorkerCommand MapCommand(MxCommand command) { ArgumentNullException.ThrowIfNull(command); // The clone is required and must stay: the caller may hand us the gRPC-owned request command, // and the caller keeps reading it after dispatch (TrackCommandReply). Cloning here is what makes // WorkerClient.CreateCommandEnvelope's no-aliasing invariant true. return new WorkerCommand { Command = command.Clone(), EnqueueTimestamp = Timestamp.FromDateTimeOffset(_timeProvider.GetUtcNow()), }; } /// /// Maps a worker command reply to a gRPC MX command reply. /// /// Worker command reply. /// The mapped , or a protocol-violation reply if the worker reply carried no public payload. public MxCommandReply MapCommandReply(WorkerCommandReply reply) { ArgumentNullException.ThrowIfNull(reply); if (reply.Reply is null) { return new MxCommandReply { ProtocolStatus = ProtocolViolation("Worker command reply did not contain a public reply payload."), }; } // ownership transfer, not a deep clone — the same rule MapEvent follows, // applied to the other (and larger, on bulk reads) hot-path message. The enclosing // WorkerCommandReply is parsed fresh from a single pipe frame in WorkerClient's read loop // and is single-consumer by construction: CompleteCommand's TryRemove hands it to exactly // one PendingCommand awaiter, that awaiter is the gRPC Invoke handler, and the handler's // one call is this mapping. Nothing else aliases or reads reply.Reply afterwards — the // enclosing WorkerCommandReply is discarded here. We therefore move the inner // MxCommandReply into the gRPC response instead of copying it; the handler owning it // outright is also what makes BulkConstraintPlan.MergeDeniedInto's in-place splice safe. // If a second consumer of the same WorkerCommandReply is ever added, restore a .Clone(). return reply.Reply; } /// /// Maps a worker event to a gRPC MX event. /// /// Worker event to map. /// The mapped , or an unspecified-family event if the worker event carried no public payload. public MxEvent MapEvent(WorkerEvent workerEvent) { ArgumentNullException.ThrowIfNull(workerEvent); // ownership transfer, not a deep clone. The enclosing WorkerEvent is // parsed fresh from a single pipe frame in WorkerClient's read loop and is discarded // immediately after this mapping — the SessionEventDistributor pump is its single // consumer (it claims the worker event channel as single-reader), so nothing else // aliases or mutates workerEvent.Event. We therefore move the inner MxEvent into the // outbound graph instead of cloning it. Downstream the pump fans this one MxEvent to // every subscriber and retains it in the replay ring, but that sharing is READ-ONLY // (subscribers only yield/filter it), so a single shared instance is safe. If a second // consumer of WorkerEvent is ever added, restore a .Clone() here to re-isolate. return workerEvent.Event ?? new MxEvent { Family = MxEventFamily.Unspecified, RawStatus = "Worker event did not contain a public event payload.", }; } /// /// Creates an OK protocol status. /// /// Status message. /// A with code . public static ProtocolStatus Ok(string message = "OK") { return new ProtocolStatus { Code = ProtocolStatusCode.Ok, Message = message, }; } /// /// Creates an InvalidRequest protocol status. /// /// Status message. /// A with code . public static ProtocolStatus InvalidRequest(string message) { return new ProtocolStatus { Code = ProtocolStatusCode.InvalidRequest, Message = message, }; } /// /// Creates a SessionNotFound protocol status. /// /// Status message. /// A with code . public static ProtocolStatus SessionNotFound(string message) { return new ProtocolStatus { Code = ProtocolStatusCode.SessionNotFound, Message = message, }; } /// /// Creates a SessionNotReady protocol status. /// /// Status message. /// A with code . public static ProtocolStatus SessionNotReady(string message) { return new ProtocolStatus { Code = ProtocolStatusCode.SessionNotReady, Message = message, }; } /// /// Creates a WorkerUnavailable protocol status. /// /// Status message. /// A with code . public static ProtocolStatus WorkerUnavailable(string message) { return new ProtocolStatus { Code = ProtocolStatusCode.WorkerUnavailable, Message = message, }; } /// /// Creates a Timeout protocol status. /// /// Status message. /// A with code . public static ProtocolStatus Timeout(string message) { return new ProtocolStatus { Code = ProtocolStatusCode.Timeout, Message = message, }; } /// /// Creates a Canceled protocol status. /// /// Status message. /// A with code . public static ProtocolStatus Canceled(string message) { return new ProtocolStatus { Code = ProtocolStatusCode.Canceled, Message = message, }; } /// /// Creates a ProtocolViolation protocol status. /// /// Status message. /// A with code . public static ProtocolStatus ProtocolViolation(string message) { return new ProtocolStatus { Code = ProtocolStatusCode.ProtocolViolation, Message = message, }; } }