using System.Buffers;
using System.Buffers.Binary;
using Google.Protobuf;
using ZB.MOM.WW.MxGateway.Contracts.Proto;
namespace ZB.MOM.WW.MxGateway.Server.Workers;
///
/// Writes length-prefixed WorkerEnvelope protobuf messages to a stream.
///
public sealed class WorkerFrameWriter
{
private readonly WorkerFrameProtocolOptions _options;
private readonly Stream _stream;
///
/// Initializes the writer with a stream and frame protocol options.
///
/// Stream to write frames to.
/// Frame protocol configuration.
public WorkerFrameWriter(
Stream stream,
WorkerFrameProtocolOptions options)
{
_stream = stream ?? throw new ArgumentNullException(nameof(stream));
_options = options ?? throw new ArgumentNullException(nameof(options));
}
///
/// Writes a WorkerEnvelope as a length-prefixed message to the stream.
///
/// Worker envelope message to write.
/// Token to cancel the asynchronous operation.
/// A task that represents the asynchronous operation.
public async ValueTask WriteAsync(
WorkerEnvelope envelope,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(envelope);
WorkerEnvelopeValidator.Validate(envelope, _options);
int payloadLength = envelope.CalculateSize();
if (payloadLength == 0)
{
throw new WorkerFrameProtocolException(
WorkerFrameProtocolErrorCode.InvalidEnvelope,
"Worker envelope cannot serialize to an empty payload.");
}
if (payloadLength > _options.MaxMessageBytes)
{
throw new WorkerFrameProtocolException(
WorkerFrameProtocolErrorCode.MessageTooLarge,
$"Worker envelope payload length {payloadLength} exceeds the configured maximum of {_options.MaxMessageBytes} bytes.");
}
// Serialize once into a single pooled buffer that carries the 4-byte little-endian length
// prefix followed by the payload, then issue one stream write. This avoids a second
// serialization pass (ToByteArray re-runs CalculateSize), a separate prefix array and its
// own write, and any per-frame heap allocation (GWC-08, IPC-13). The rented buffer may be
// larger than requested, so only the first frameLength bytes are ever written.
int frameLength = sizeof(uint) + payloadLength;
byte[] frame = ArrayPool.Shared.Rent(frameLength);
try
{
BinaryPrimitives.WriteUInt32LittleEndian(frame, (uint)payloadLength);
envelope.WriteTo(new Span(frame, sizeof(uint), payloadLength));
await _stream
.WriteAsync(new ReadOnlyMemory(frame, 0, frameLength), cancellationToken)
.ConfigureAwait(false);
}
finally
{
ArrayPool.Shared.Return(frame);
}
}
}