feat(client): add rendezvous traversal coordinators (#12)
quality-gate / quality (push) Successful in 56s
quality-gate / quality (push) Successful in 56s
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
using FinalFactory.Rendezvous.Contracts;
|
||||
|
||||
namespace FinalFactory.Rendezvous.Client;
|
||||
|
||||
public sealed class RendezvousJoinClient : IRendezvousJoinClient
|
||||
{
|
||||
private const string LeaseTokenHeader = "X-Rendezvous-Lease-Token";
|
||||
private const string ClientPunchCapabilityHeader = "X-Rendezvous-Client-Punch-Capability";
|
||||
|
||||
private readonly RendezvousHttpTransport _transport;
|
||||
|
||||
public RendezvousJoinClient(
|
||||
HttpClient httpClient,
|
||||
RendezvousClientOptions? options = null,
|
||||
IRendezvousDelay? delay = null)
|
||||
{
|
||||
_transport = new(httpClient, options, delay);
|
||||
}
|
||||
|
||||
public Task<RendezvousClientResult<CreateJoinAttemptResponse>> CreateAsync(
|
||||
CreateJoinAttemptRequest request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (request is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(request));
|
||||
}
|
||||
CreateJoinAttemptRequest body = new()
|
||||
{
|
||||
ContractVersion = request.ContractVersion,
|
||||
IdempotencyKey = request.IdempotencyKey,
|
||||
GameId = request.GameId,
|
||||
EnvironmentId = request.EnvironmentId,
|
||||
ListingId = request.ListingId,
|
||||
ProtocolVersion = request.ProtocolVersion,
|
||||
};
|
||||
return _transport.SendSafeAsync<CreateJoinAttemptResponse>(
|
||||
() => RendezvousHttpTransport.JsonRequest(HttpMethod.Post, "v1/join-attempts", body),
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
public Task<RendezvousClientResult<bool>> CancelAsync(
|
||||
CreateJoinAttemptResponse attempt,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (attempt is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(attempt));
|
||||
}
|
||||
return _transport.SendSafeAsync<bool>(
|
||||
() => HeaderRequest(
|
||||
HttpMethod.Delete,
|
||||
$"v1/join-attempts/{attempt.AttemptId}",
|
||||
ClientPunchCapabilityHeader,
|
||||
RequireHeaderValue(attempt.ClientPunchCapability, nameof(attempt))),
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
public Task<RendezvousClientResult<BrowseHostJoinAttemptsResponse>> BrowseForHostAsync(
|
||||
PublishedSession session,
|
||||
int pageSize = ContractLimits.BrowserPageMaxItems,
|
||||
string? cursor = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (session is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(session));
|
||||
}
|
||||
if (pageSize is < 1 or > ContractLimits.BrowserPageMaxItems)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(pageSize));
|
||||
}
|
||||
|
||||
string query = $"v1/sessions/{session.ListingId}/join-attempts"
|
||||
+ $"?contractVersion={ContractLimits.ContractVersion}"
|
||||
+ $"&pageSize={pageSize}"
|
||||
+ (cursor is null ? string.Empty : $"&cursor={Uri.EscapeDataString(cursor)}");
|
||||
return _transport.SendSafeAsync<BrowseHostJoinAttemptsResponse>(
|
||||
() => HeaderRequest(
|
||||
HttpMethod.Get,
|
||||
query,
|
||||
LeaseTokenHeader,
|
||||
RequireHeaderValue(session.LeaseToken, nameof(session))),
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<RendezvousClientResult<IReadOnlyList<HostJoinAttempt>>> BrowseAllForHostAsync(
|
||||
PublishedSession session,
|
||||
int maximumPages = 100,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (session is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(session));
|
||||
}
|
||||
if (maximumPages is < 1 or > 1_000)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(maximumPages));
|
||||
}
|
||||
|
||||
List<HostJoinAttempt> attempts = [];
|
||||
string? cursor = null;
|
||||
for (int page = 0; page < maximumPages; page++)
|
||||
{
|
||||
RendezvousClientResult<BrowseHostJoinAttemptsResponse> result =
|
||||
await BrowseForHostAsync(
|
||||
session,
|
||||
ContractLimits.BrowserPageMaxItems,
|
||||
cursor,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
if (!result.IsSuccess || result.Value is null)
|
||||
{
|
||||
return RendezvousClientResult.Failure<IReadOnlyList<HostJoinAttempt>>(
|
||||
result.Error,
|
||||
result.Message,
|
||||
result.RetryAfterSeconds);
|
||||
}
|
||||
|
||||
attempts.AddRange(result.Value.Items);
|
||||
cursor = result.Value.NextCursor;
|
||||
if (string.IsNullOrEmpty(cursor))
|
||||
{
|
||||
return RendezvousClientResult.Success<IReadOnlyList<HostJoinAttempt>>(
|
||||
attempts.AsReadOnly());
|
||||
}
|
||||
}
|
||||
|
||||
return RendezvousClientResult.Failure<IReadOnlyList<HostJoinAttempt>>(
|
||||
RendezvousErrorCode.CapacityExceeded,
|
||||
$"Host invitation polling exceeded the configured {maximumPages}-page limit.");
|
||||
}
|
||||
|
||||
private static HttpRequestMessage HeaderRequest(
|
||||
HttpMethod method,
|
||||
string uri,
|
||||
string header,
|
||||
string value)
|
||||
{
|
||||
HttpRequestMessage request = new(method, uri);
|
||||
request.Headers.TryAddWithoutValidation(header, value);
|
||||
return request;
|
||||
}
|
||||
|
||||
private static string RequireHeaderValue(string value, string parameterName) =>
|
||||
!string.IsNullOrWhiteSpace(value)
|
||||
? value
|
||||
: throw new ArgumentException("The required capability is missing.", parameterName);
|
||||
}
|
||||
Reference in New Issue
Block a user