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