226 lines
7.2 KiB
C#
226 lines
7.2 KiB
C#
using FinalFactory.Rendezvous.Contracts;
|
|
|
|
namespace FinalFactory.Rendezvous.Client;
|
|
|
|
public sealed class RendezvousClientResult<T>
|
|
{
|
|
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<T> Success<T>(T value) =>
|
|
value is null
|
|
? throw new ArgumentNullException(nameof(value))
|
|
: new(RendezvousErrorCode.None, value, string.Empty, null);
|
|
|
|
public static RendezvousClientResult<T> Failure<T>(
|
|
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<RendezvousClientResult<PublishedSession>> RegisterAsync(
|
|
RegisterSessionRequest request,
|
|
string publisherCredential,
|
|
CancellationToken cancellationToken = default);
|
|
|
|
Task<RendezvousClientResult<RenewLeaseResponse>> RenewAsync(
|
|
PublishedSession session,
|
|
string publisherCredential,
|
|
CancellationToken cancellationToken = default);
|
|
|
|
Task<RendezvousClientResult<bool>> UpdateAsync(
|
|
PublishedSession session,
|
|
UpdateSessionRequest request,
|
|
string publisherCredential,
|
|
CancellationToken cancellationToken = default);
|
|
|
|
Task<RendezvousClientResult<bool>> DeregisterAsync(
|
|
PublishedSession session,
|
|
string publisherCredential,
|
|
CancellationToken cancellationToken = default);
|
|
}
|
|
|
|
public interface IRendezvousSessionBrowserClient
|
|
{
|
|
Task<RendezvousClientResult<BrowseSessionsResponse>> BrowseAsync(
|
|
BrowseSessionsRequest request,
|
|
CancellationToken cancellationToken = default);
|
|
|
|
Task<RendezvousClientResult<IReadOnlyList<SessionListing>>> BrowseAllAsync(
|
|
BrowseSessionsRequest request,
|
|
int maximumPages = 100,
|
|
CancellationToken cancellationToken = default);
|
|
|
|
Task<RendezvousClientResult<GetSessionResponse>> GetAsync(
|
|
SessionListingId listingId,
|
|
GameId gameId,
|
|
EnvironmentId environmentId,
|
|
uint protocolVersion,
|
|
CancellationToken cancellationToken = default);
|
|
}
|
|
|
|
public interface IRendezvousJoinClient
|
|
{
|
|
Task<RendezvousConnectionStartResult> CreateConnectionAttemptAsync(
|
|
CreateJoinAttemptRequest request,
|
|
NetworkEndpoint? dedicatedFallback = null,
|
|
CancellationToken cancellationToken = default);
|
|
|
|
Task<RendezvousClientResult<CreateJoinAttemptResponse>> CreateAsync(
|
|
CreateJoinAttemptRequest request,
|
|
CancellationToken cancellationToken = default);
|
|
|
|
Task<RendezvousClientResult<bool>> CancelAsync(
|
|
CreateJoinAttemptResponse attempt,
|
|
CancellationToken cancellationToken = default);
|
|
|
|
Task<RendezvousClientResult<BrowseHostJoinAttemptsResponse>> BrowseForHostAsync(
|
|
PublishedSession session,
|
|
int pageSize = ContractLimits.BrowserPageMaxItems,
|
|
string? cursor = null,
|
|
CancellationToken cancellationToken = default);
|
|
|
|
Task<RendezvousClientResult<IReadOnlyList<HostJoinAttempt>>> BrowseAllForHostAsync(
|
|
PublishedSession session,
|
|
int maximumPages = 100,
|
|
CancellationToken cancellationToken = default);
|
|
|
|
Task<RendezvousClientResult<ReportConnectionOutcomeResponse>> 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,
|
|
};
|
|
}
|