V2/Networking/OperationResultWireContract.cs

Static helper that encodes and decodes a minimal wire contract for operation results, specifically handling rate-limit results by storing a retry-after value (in milliseconds) in the error details. It provides methods to create a RateLimited OperationResult, extract the retry interval from an OperationResult, decode a result from primitive fields, and normalize retry durations to safe bounds.

Networking
#nullable enable

using System;
using System.Collections.Generic;
using System.Globalization;
using Hexagon.V2.Kernel;

namespace Hexagon.V2.Networking;

/// <summary>
/// Minimal, bounded transport contract for command results. The RPC surface stays
/// scalar-only while preserving the retry delay required by rate-limit failures.
/// </summary>
public static class OperationResultWireContract
{
	public const string RetryAfterMillisecondsDetail = "retry_after_ms";
	public const int DefaultRetryAfterMilliseconds = 125;
	public const int MaximumRetryAfterMilliseconds = 60_000;

	public static OperationResult RateLimited( string message, TimeSpan retryAfter )
	{
		var milliseconds = Normalize( retryAfter.TotalMilliseconds );
		return OperationResult.Failure(
			ErrorCode.RateLimited,
			message,
			new Dictionary<string, string>( StringComparer.Ordinal )
			{
				[RetryAfterMillisecondsDetail] = milliseconds.ToString( CultureInfo.InvariantCulture )
			} );
	}

	public static int EncodeRetryAfterMilliseconds( OperationResult result )
	{
		if ( result.Succeeded || result.Error?.Code != ErrorCode.RateLimited ) return 0;
		if ( result.Error.Details?.TryGetValue( RetryAfterMillisecondsDetail, out var encoded ) != true ||
			!long.TryParse( encoded, NumberStyles.None, CultureInfo.InvariantCulture, out var parsed ) )
			return DefaultRetryAfterMilliseconds;
		return Normalize( parsed );
	}

	public static OperationResult Decode(
		bool succeeded,
		int rawErrorCode,
		string? message,
		int retryAfterMilliseconds )
	{
		if ( succeeded ) return OperationResult.Success();
		var code = Enum.IsDefined( typeof(ErrorCode), rawErrorCode ) && rawErrorCode != (int)ErrorCode.None
			? (ErrorCode)rawErrorCode
			: ErrorCode.InternalError;
		var safeMessage = string.IsNullOrWhiteSpace( message ) ? "The host denied the command." : message;
		if ( code != ErrorCode.RateLimited ) return OperationResult.Failure( code, safeMessage );
		var normalized = Normalize( retryAfterMilliseconds );
		return OperationResult.Failure(
			code,
			safeMessage,
			new Dictionary<string, string>( StringComparer.Ordinal )
			{
				[RetryAfterMillisecondsDetail] = normalized.ToString( CultureInfo.InvariantCulture )
			} );
	}

	private static int Normalize( double milliseconds )
	{
		if ( double.IsNaN( milliseconds ) || milliseconds <= 0d ) return DefaultRetryAfterMilliseconds;
		if ( double.IsPositiveInfinity( milliseconds ) || milliseconds >= MaximumRetryAfterMilliseconds )
			return MaximumRetryAfterMilliseconds;
		return Math.Clamp( (int)Math.Ceiling( milliseconds ), 1, MaximumRetryAfterMilliseconds );
	}

	private static int Normalize( long milliseconds ) => milliseconds switch
	{
		<= 0 => DefaultRetryAfterMilliseconds,
		>= MaximumRetryAfterMilliseconds => MaximumRetryAfterMilliseconds,
		_ => (int)milliseconds
	};
}