using FinalFactory.Rendezvous.Contracts; namespace FinalFactory.Rendezvous.Client; public sealed class RendezvousClientResult { internal RendezvousClientResult( RendezvousErrorCode error, T? value, string message, int? retryAfterSeconds) { Error = error; Value = value; Message = message; RetryAfterSeconds = retryAfterSeconds; } public bool IsSuccess => Error == RendezvousErrorCode.None; public RendezvousErrorCode Error { get; } public T? Value { get; } public string Message { get; } public int? RetryAfterSeconds { get; } } public static class RendezvousClientResult { public static RendezvousClientResult Success(T value) => value is null ? throw new ArgumentNullException(nameof(value)) : new(RendezvousErrorCode.None, value, string.Empty, null); public static RendezvousClientResult Failure( RendezvousErrorCode error, string message, int? retryAfterSeconds = null) => error == RendezvousErrorCode.None ? throw new ArgumentException("A failure requires a non-success error.", nameof(error)) : new(error, default, message ?? string.Empty, retryAfterSeconds); } public sealed class PublishedSession { private readonly object _timingGate = new(); private DateTimeOffset _expiresAt; private int _leaseRenewAfterSeconds; internal PublishedSession(RegisterSessionResponse response) { ListingId = response.ListingId; LeaseId = response.LeaseId; LeaseToken = response.LeaseToken; HostPresenceHandle = response.HostPresenceHandle; HostPresenceCapability = response.HostPresenceCapability; _expiresAt = response.ExpiresAt; _leaseRenewAfterSeconds = response.LeaseRenewAfterSeconds; HostPresenceRefreshAfterSeconds = response.HostPresenceRefreshAfterSeconds; } public SessionListingId ListingId { get; } public LeaseId LeaseId { get; } public string LeaseToken { get; } public MediationHandle HostPresenceHandle { get; } public string HostPresenceCapability { get; } public DateTimeOffset ExpiresAt { get { lock (_timingGate) { return _expiresAt; } } internal set { lock (_timingGate) { _expiresAt = value; } } } public int LeaseRenewAfterSeconds { get { lock (_timingGate) { return _leaseRenewAfterSeconds; } } internal set { lock (_timingGate) { _leaseRenewAfterSeconds = value; } } } public int HostPresenceRefreshAfterSeconds { get; } public override string ToString() => $"[PublishedSession {ListingId}; credentials redacted]"; } public interface IRendezvousPublisherClient { Task> RegisterAsync( RegisterSessionRequest request, string publisherCredential, CancellationToken cancellationToken = default); Task> RenewAsync( PublishedSession session, string publisherCredential, CancellationToken cancellationToken = default); Task> UpdateAsync( PublishedSession session, UpdateSessionRequest request, string publisherCredential, CancellationToken cancellationToken = default); Task> DeregisterAsync( PublishedSession session, string publisherCredential, CancellationToken cancellationToken = default); } public interface IRendezvousSessionBrowserClient { Task> BrowseAsync( BrowseSessionsRequest request, CancellationToken cancellationToken = default); Task>> BrowseAllAsync( BrowseSessionsRequest request, int maximumPages = 100, CancellationToken cancellationToken = default); Task> GetAsync( SessionListingId listingId, GameId gameId, EnvironmentId environmentId, uint protocolVersion, CancellationToken cancellationToken = default); } public interface IRendezvousJoinClient { Task CreateConnectionAttemptAsync( CreateJoinAttemptRequest request, NetworkEndpoint? dedicatedFallback = null, CancellationToken cancellationToken = default); Task> CreateAsync( CreateJoinAttemptRequest request, CancellationToken cancellationToken = default); Task> CancelAsync( CreateJoinAttemptResponse attempt, CancellationToken cancellationToken = default); Task> BrowseForHostAsync( PublishedSession session, int pageSize = ContractLimits.BrowserPageMaxItems, string? cursor = null, CancellationToken cancellationToken = default); Task>> BrowseAllForHostAsync( PublishedSession session, int maximumPages = 100, CancellationToken cancellationToken = default); Task> ReportOutcomeAsync( CreateJoinAttemptResponse attempt, RendezvousConnectionOutcome outcome, CancellationToken cancellationToken = default); } public interface IRendezvousDelay { Task DelayAsync(TimeSpan delay, CancellationToken cancellationToken); } public sealed class RendezvousClientOptions { public int MaximumSafeRetries { get; set; } = 2; public TimeSpan RequestTimeout { get; set; } = TimeSpan.FromSeconds(5); public TimeSpan InitialRetryDelay { get; set; } = TimeSpan.FromMilliseconds(200); public TimeSpan MaximumRetryDelay { get; set; } = TimeSpan.FromSeconds(2); public double JitterRatio { get; set; } = 0.2; internal void Validate() { if (MaximumSafeRetries is < 0 or > 5 || RequestTimeout <= TimeSpan.Zero || RequestTimeout > TimeSpan.FromSeconds(30) || InitialRetryDelay < TimeSpan.Zero || MaximumRetryDelay < InitialRetryDelay || MaximumRetryDelay > TimeSpan.FromSeconds(30) || JitterRatio is < 0 or > 1) { throw new ArgumentOutOfRangeException(nameof(RendezvousClientOptions)); } } } internal sealed class SystemRendezvousDelay : IRendezvousDelay { public Task DelayAsync(TimeSpan delay, CancellationToken cancellationToken) => Task.Delay(delay, cancellationToken); } internal static class RendezvousEndpoint { internal static NetworkEndpoint? Copy(NetworkEndpoint? endpoint) => endpoint is null ? null : new NetworkEndpoint { AddressFamily = endpoint.AddressFamily, Address = endpoint.Address, Port = endpoint.Port, }; }