using System; using System.IO; using System.Threading; using System.Threading.Tasks; using Google.Protobuf; using MxGateway.Contracts.Proto; namespace MxGateway.Worker.Ipc; public sealed class WorkerFrameWriter { private readonly WorkerFrameProtocolOptions _options; private readonly SemaphoreSlim _writeLock = new(1, 1); private readonly Stream _stream; public WorkerFrameWriter( Stream stream, WorkerFrameProtocolOptions options) { _stream = stream ?? throw new ArgumentNullException(nameof(stream)); _options = options ?? throw new ArgumentNullException(nameof(options)); } public async Task WriteAsync( WorkerEnvelope envelope, CancellationToken cancellationToken = default) { if (envelope is null) { throw new ArgumentNullException(nameof(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."); } byte[] payload = envelope.ToByteArray(); byte[] lengthPrefix = new byte[sizeof(uint)]; WriteUInt32LittleEndian(lengthPrefix, (uint)payloadLength); await _writeLock.WaitAsync(cancellationToken).ConfigureAwait(false); try { await _stream.WriteAsync(lengthPrefix, 0, lengthPrefix.Length, cancellationToken).ConfigureAwait(false); await _stream.WriteAsync(payload, 0, payload.Length, cancellationToken).ConfigureAwait(false); await _stream.FlushAsync(cancellationToken).ConfigureAwait(false); } finally { _writeLock.Release(); } } private static void WriteUInt32LittleEndian( byte[] buffer, uint value) { buffer[0] = (byte)value; buffer[1] = (byte)(value >> 8); buffer[2] = (byte)(value >> 16); buffer[3] = (byte)(value >> 24); } }