feat: add publisher and browser client SDK (#9)
quality-gate / quality (push) Successful in 1m6s

Closes #9
This commit is contained in:
KyuubiYoru
2026-07-16 06:27:44 +02:00
parent a9a2b3db35
commit 06c3973ce7
13 changed files with 1534 additions and 1 deletions
@@ -0,0 +1,342 @@
using System.Net;
using System.Text;
using System.Text.Json;
using FinalFactory.Rendezvous.Client;
using FinalFactory.Rendezvous.Contracts;
namespace FinalFactory.Rendezvous.Tests.Client;
public sealed class RendezvousClientBehaviorTests
{
[Fact]
public async Task RegistrationRetriesWithTheSameIdempotentPayloadAndDisposesResponses()
{
TrackingContent unavailable = JsonContent(new ApiError
{
Code = RendezvousErrorCode.ServiceUnavailable,
Message = "try later",
RetryAfterSeconds = 1,
});
TrackingContent created = JsonContent(CreateRegistrationResponse());
ScriptedHandler handler = new(
Response(HttpStatusCode.ServiceUnavailable, unavailable),
Response(HttpStatusCode.Created, created),
new HttpResponseMessage(HttpStatusCode.NoContent));
using HttpClient httpClient = new(handler) { BaseAddress = new("http://rendezvous.test/") };
RegisterSessionRequest request = CreateRegistrationRequest("stable-idempotency-key");
RecordingDelay delay = new(() => request.DisplayName = "mutated during retry delay");
RendezvousPublisherClient publisher = new(
httpClient,
new RendezvousClientOptions { JitterRatio = 0 },
delay);
RendezvousClientResult<PublishedSession> result = await publisher.RegisterAsync(
request,
"publisher-credential");
Assert.True(result.IsSuccess);
Assert.Equal(2, handler.RequestBodies.Count);
Assert.Equal(handler.RequestBodies[0], handler.RequestBodies[1]);
Assert.Contains("stable-idempotency-key", handler.RequestBodies[0], StringComparison.Ordinal);
Assert.Equal(TimeSpan.FromSeconds(1), Assert.Single(delay.Delays));
Assert.True(unavailable.IsDisposed);
Assert.True(created.IsDisposed);
using HttpResponseMessage stillOwnedByCaller = await httpClient.GetAsync("health");
Assert.Equal(HttpStatusCode.NoContent, stillOwnedByCaller.StatusCode);
}
[Fact]
public async Task UpdateUsesTheLeaseWithoutMutatingTheCallersRequest()
{
ScriptedHandler handler = new(
Response(HttpStatusCode.Created, JsonContent(CreateRegistrationResponse())),
new HttpResponseMessage(HttpStatusCode.NoContent));
using HttpClient httpClient = new(handler) { BaseAddress = new("http://rendezvous.test/") };
RendezvousPublisherClient publisher = new(httpClient);
PublishedSession session = AssertSuccess(await publisher.RegisterAsync(
CreateRegistrationRequest("update-idempotency-key"),
"publisher-credential"));
UpdateSessionRequest update = new()
{
LeaseToken = "caller-placeholder",
BuildVersion = "2.0.0",
DisplayName = "updated",
Capacity = new() { CurrentPlayers = 2, MaximumPlayers = 4 },
Metadata = new() { ["mode"] = "online-coop" },
};
RendezvousClientResult<bool> result = await publisher.UpdateAsync(
session,
update,
"publisher-credential");
Assert.True(result.IsSuccess);
Assert.Equal("caller-placeholder", update.LeaseToken);
Assert.Contains("lease-token", handler.RequestBodies[1], StringComparison.Ordinal);
Assert.DoesNotContain("caller-placeholder", handler.RequestBodies[1], StringComparison.Ordinal);
}
[Fact]
public async Task GatewayFailureIsRetriedForSafeBrowserReads()
{
ScriptedHandler handler = new(
new HttpResponseMessage(HttpStatusCode.BadGateway),
Response(HttpStatusCode.OK, JsonContent(new BrowseSessionsResponse())));
using HttpClient httpClient = new(handler) { BaseAddress = new("http://rendezvous.test/") };
RecordingDelay delay = new();
RendezvousSessionBrowserClient browser = new(
httpClient,
new RendezvousClientOptions { JitterRatio = 0 },
delay);
RendezvousClientResult<BrowseSessionsResponse> result = await browser.BrowseAsync(new()
{
GameId = new("space-game"),
EnvironmentId = new("production"),
ProtocolVersion = 7,
});
Assert.True(result.IsSuccess, result.Message);
Assert.Equal(2, handler.RequestUris.Count);
Assert.Equal(TimeSpan.FromMilliseconds(200), Assert.Single(delay.Delays));
}
[Fact]
public void SuccessResultRequiresAValue()
{
Assert.Throws<ArgumentNullException>(() => RendezvousClientResult.Success<string>(null!));
}
[Fact]
public async Task BrowseAllFollowsCursorsWithoutMutatingTheCallersRequest()
{
ScriptedHandler handler = new(
Response(HttpStatusCode.OK, JsonContent(new BrowseSessionsResponse
{
Items = [CreateListing("00000000-0000-0000-0000-000000000001")],
NextCursor = "next page+token",
})),
Response(HttpStatusCode.OK, JsonContent(new BrowseSessionsResponse
{
Items = [CreateListing("00000000-0000-0000-0000-000000000002")],
})));
using HttpClient httpClient = new(handler) { BaseAddress = new("http://rendezvous.test/") };
RendezvousSessionBrowserClient browser = new(httpClient);
BrowseSessionsRequest request = new()
{
GameId = new("space-game"),
EnvironmentId = new("production"),
ProtocolVersion = 7,
PageSize = 1,
};
RendezvousClientResult<IReadOnlyList<SessionListing>> result = await browser.BrowseAllAsync(request);
Assert.True(result.IsSuccess);
Assert.Equal(2, result.Value!.Count);
Assert.Null(request.Cursor);
Assert.DoesNotContain("cursor=", handler.RequestUris[0].Query, StringComparison.Ordinal);
Assert.Contains("cursor=next%20page%2Btoken", handler.RequestUris[1].Query, StringComparison.Ordinal);
Assert.Contains("gameId=space-game", handler.RequestUris[0].Query, StringComparison.Ordinal);
}
[Fact]
public async Task LeaseMaintainerReportsLeaseLoss()
{
ScriptedHandler handler = new(
Response(HttpStatusCode.Created, JsonContent(CreateRegistrationResponse())),
Response(HttpStatusCode.Gone, JsonContent(new ApiError
{
Code = RendezvousErrorCode.Expired,
Message = "lease expired",
})));
using HttpClient httpClient = new(handler) { BaseAddress = new("http://rendezvous.test/") };
RecordingDelay delay = new();
RendezvousPublisherClient publisher = new(httpClient, delay: delay);
PublishedSession session = AssertSuccess(await publisher.RegisterAsync(
CreateRegistrationRequest("lease-loss-key"),
"publisher-credential"));
await using SessionLeaseMaintainer maintainer = publisher.CreateLeaseMaintainer(
session,
"publisher-credential");
bool eventRaised = false;
maintainer.LeaseLost += (_, _) => eventRaised = true;
LeaseMaintenanceResult result = await maintainer.RunAsync();
Assert.Equal(LeaseMaintenanceStopReason.LeaseLost, result.Reason);
Assert.Equal(RendezvousErrorCode.Expired, result.Error);
Assert.True(eventRaised);
Assert.Equal(TimeSpan.FromSeconds(15), Assert.Single(delay.Delays));
}
[Fact]
public async Task DisposingLeaseMaintainerCancelsItsWaitAndDoesNotRenew()
{
ScriptedHandler handler = new(
Response(HttpStatusCode.Created, JsonContent(CreateRegistrationResponse())));
using HttpClient httpClient = new(handler) { BaseAddress = new("http://rendezvous.test/") };
BlockingDelay delay = new();
RendezvousPublisherClient publisher = new(httpClient, delay: delay);
PublishedSession session = AssertSuccess(await publisher.RegisterAsync(
CreateRegistrationRequest("dispose-key"),
"publisher-credential"));
SessionLeaseMaintainer maintainer = publisher.CreateLeaseMaintainer(
session,
"publisher-credential");
Task<LeaseMaintenanceResult> active = maintainer.RunAsync();
await delay.Started.Task.WaitAsync(TimeSpan.FromSeconds(2));
await maintainer.DisposeAsync();
LeaseMaintenanceResult result = await active;
Assert.Equal(LeaseMaintenanceStopReason.Disposed, result.Reason);
Assert.Single(handler.RequestUris);
await Assert.ThrowsAsync<ObjectDisposedException>(() => maintainer.RunAsync());
}
[Fact]
public async Task CallerCancellationStopsLeaseMaintenanceWithoutRenewing()
{
ScriptedHandler handler = new(
Response(HttpStatusCode.Created, JsonContent(CreateRegistrationResponse())));
using HttpClient httpClient = new(handler) { BaseAddress = new("http://rendezvous.test/") };
BlockingDelay delay = new();
RendezvousPublisherClient publisher = new(httpClient, delay: delay);
PublishedSession session = AssertSuccess(await publisher.RegisterAsync(
CreateRegistrationRequest("cancel-key"),
"publisher-credential"));
await using SessionLeaseMaintainer maintainer = publisher.CreateLeaseMaintainer(
session,
"publisher-credential");
using CancellationTokenSource cancellation = new();
Task<LeaseMaintenanceResult> active = maintainer.RunAsync(cancellation.Token);
await delay.Started.Task.WaitAsync(TimeSpan.FromSeconds(2));
await cancellation.CancelAsync();
LeaseMaintenanceResult result = await active;
Assert.Equal(LeaseMaintenanceStopReason.Cancelled, result.Reason);
Assert.Single(handler.RequestUris);
}
private static T AssertSuccess<T>(RendezvousClientResult<T> result)
{
Assert.True(result.IsSuccess, result.Message);
return Assert.IsType<T>(result.Value);
}
private static RegisterSessionRequest CreateRegistrationRequest(string idempotencyKey) => new()
{
IdempotencyKey = idempotencyKey,
GameId = new("space-game"),
EnvironmentId = new("production"),
RegionId = new("eu-central"),
ProtocolVersion = 7,
BuildVersion = "1.0.0",
DisplayName = "SDK host",
Visibility = ListingVisibility.Public,
Capacity = new() { CurrentPlayers = 1, MaximumPlayers = 4 },
};
private static RegisterSessionResponse CreateRegistrationResponse() => new()
{
ListingId = new(Guid.Parse("00000000-0000-0000-0000-000000000010")),
LeaseId = new(Guid.Parse("00000000-0000-0000-0000-000000000011")),
LeaseToken = "lease-token",
HostPresenceHandle = new(Guid.Parse("00000000-0000-0000-0000-000000000012")),
HostPresenceCapability = "presence-capability",
ExpiresAt = new DateTimeOffset(2030, 1, 1, 0, 0, 0, TimeSpan.Zero),
LeaseRenewAfterSeconds = 15,
HostPresenceRefreshAfterSeconds = 10,
};
private static SessionListing CreateListing(string id) => new()
{
ListingId = new(Guid.Parse(id)),
GameId = new("space-game"),
EnvironmentId = new("production"),
RegionId = new("eu-central"),
ProtocolVersion = 7,
BuildVersion = "1.0.0",
DisplayName = "host",
Visibility = ListingVisibility.Public,
PublisherTrustMode = PublisherTrustMode.ManagedDedicated,
Capacity = new() { CurrentPlayers = 1, MaximumPlayers = 4 },
};
private static TrackingContent JsonContent<T>(T value) => new(
JsonSerializer.SerializeToUtf8Bytes(value, ContractJson.Options));
private static HttpResponseMessage Response(HttpStatusCode status, HttpContent content) => new(status)
{
Content = content,
};
private sealed class ScriptedHandler(params HttpResponseMessage[] responses) : HttpMessageHandler
{
private readonly Queue<HttpResponseMessage> _responses = new(responses);
internal List<string> RequestBodies { get; } = [];
internal List<Uri> RequestUris { get; } = [];
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
RequestUris.Add(request.RequestUri!);
RequestBodies.Add(request.Content is null
? string.Empty
: await request.Content.ReadAsStringAsync(cancellationToken));
return _responses.Count > 0
? _responses.Dequeue()
: throw new InvalidOperationException("No scripted response remains.");
}
}
private sealed class TrackingContent(byte[] bytes) : HttpContent
{
internal bool IsDisposed { get; private set; }
protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context) =>
stream.WriteAsync(bytes).AsTask();
protected override bool TryComputeLength(out long length)
{
length = bytes.Length;
return true;
}
protected override void Dispose(bool disposing)
{
IsDisposed = true;
base.Dispose(disposing);
}
}
private sealed class RecordingDelay(Action? onDelay = null) : IRendezvousDelay
{
internal List<TimeSpan> Delays { get; } = [];
public Task DelayAsync(TimeSpan delay, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
Delays.Add(delay);
onDelay?.Invoke();
return Task.CompletedTask;
}
}
private sealed class BlockingDelay : IRendezvousDelay
{
internal TaskCompletionSource Started { get; } = new(
TaskCreationOptions.RunContinuationsAsynchronously);
public Task DelayAsync(TimeSpan delay, CancellationToken cancellationToken)
{
Started.TrySetResult();
return Task.Delay(Timeout.InfiniteTimeSpan, cancellationToken);
}
}
}
@@ -0,0 +1,195 @@
using FinalFactory.Rendezvous.Client;
using FinalFactory.Rendezvous.Contracts;
using FinalFactory.Rendezvous.Server.Browser;
using FinalFactory.Rendezvous.Server.Http;
using FinalFactory.Rendezvous.Server.Provisioning;
using FinalFactory.Rendezvous.Server.Sessions;
using FinalFactory.Rendezvous.Server.State;
using FinalFactory.Rendezvous.Tests.Provisioning;
using FinalFactory.Rendezvous.Tests.State;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Hosting.Server.Features;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
namespace FinalFactory.Rendezvous.Tests.Client;
public sealed class RendezvousClientIntegrationTests
{
[Fact]
public async Task PublisherAndBrowserClientsCompleteTheRealSessionLifecycleAndPaging()
{
await using ClientTestHost host = await ClientTestHost.StartAsync();
RendezvousPublisherClient publisher = new(host.HttpClient);
RendezvousSessionBrowserClient browser = new(host.HttpClient);
List<PublishedSession> sessions = [];
for (int index = 0; index < 3; index++)
{
RendezvousClientResult<PublishedSession> registered = await publisher.RegisterAsync(
CreateRegistration(index),
host.PublisherCredential);
PublishedSession session = AssertSuccess(registered);
sessions.Add(session);
Assert.True(host.Capabilities.TryFingerprint(
session.HostPresenceCapability,
out SecretFingerprint fingerprint));
StoreResult<StoredListing> bound = host.Store.BindHostPresence(new(
session.HostPresenceHandle,
fingerprint,
new(AddressFamilyKind.Ipv4, $"203.0.113.{80 + index}", 41_000 + index),
null));
Assert.Equal(StoreResultCode.Success, bound.Code);
}
PublishedSession first = sessions[0];
RendezvousClientResult<RenewLeaseResponse> renewed = await publisher.RenewAsync(
first,
host.PublisherCredential);
Assert.True(renewed.IsSuccess, renewed.Message);
Assert.Equal(renewed.Value!.ExpiresAt, first.ExpiresAt);
UpdateSessionRequest update = new()
{
BuildVersion = "2.0.0",
DisplayName = "SDK host updated",
Capacity = new() { CurrentPlayers = 2, MaximumPlayers = 8 },
Metadata = new() { ["mode"] = "online-coop" },
};
RendezvousClientResult<bool> updated = await publisher.UpdateAsync(
first,
update,
host.PublisherCredential);
Assert.True(updated.IsSuccess, updated.Message);
BrowseSessionsRequest browseRequest = new()
{
GameId = new("space-game"),
EnvironmentId = new("production"),
RegionId = new("eu-central"),
ProtocolVersion = 7,
PageSize = 1,
ExcludeFull = true,
};
IReadOnlyList<SessionListing> listings = AssertSuccess(
await browser.BrowseAllAsync(browseRequest));
Assert.Equal(3, listings.Count);
Assert.Equal("SDK host updated", listings.Single(item => item.ListingId == first.ListingId).DisplayName);
GetSessionResponse direct = AssertSuccess(await browser.GetAsync(
first.ListingId,
new("space-game"),
new("production"),
7));
Assert.Equal("2.0.0", direct.Session.BuildVersion);
foreach (PublishedSession session in sessions)
{
RendezvousClientResult<bool> deregistered = await publisher.DeregisterAsync(
session,
host.PublisherCredential);
Assert.True(deregistered.IsSuccess, deregistered.Message);
Assert.Equal(StoreResultCode.NotFound, host.Store.GetListing(session.ListingId, false).Code);
}
}
private static T AssertSuccess<T>(RendezvousClientResult<T> result)
{
Assert.True(result.IsSuccess, result.Message);
return Assert.IsAssignableFrom<T>(result.Value);
}
private static RegisterSessionRequest CreateRegistration(int index) => new()
{
IdempotencyKey = $"sdk-integration-{index}",
GameId = new("space-game"),
EnvironmentId = new("production"),
RegionId = new("eu-central"),
ProtocolVersion = 7,
BuildVersion = "1.0.0",
DisplayName = $"SDK host {index}",
Visibility = ListingVisibility.Public,
Capacity = new() { CurrentPlayers = 1, MaximumPlayers = 8 },
Metadata = new() { ["mode"] = "online-coop" },
};
private sealed class ClientTestHost : IAsyncDisposable
{
private readonly WebApplication _application;
private ClientTestHost(
WebApplication application,
HttpClient httpClient,
InMemoryEphemeralRendezvousStore store,
EphemeralCapabilityIssuer capabilities,
string publisherCredential)
{
_application = application;
HttpClient = httpClient;
Store = store;
Capabilities = capabilities;
PublisherCredential = publisherCredential;
}
internal HttpClient HttpClient { get; }
internal InMemoryEphemeralRendezvousStore Store { get; }
internal EphemeralCapabilityIssuer Capabilities { get; }
internal string PublisherCredential { get; }
internal static async Task<ClientTestHost> StartAsync()
{
ManualRendezvousClock clock = new(ProvisioningTestData.Now);
EphemeralStoreOptions stateOptions = new();
InMemoryEphemeralRendezvousStore store = new(stateOptions, clock, clock);
EphemeralCapabilityIssuer capabilities = new();
ProvisioningRuntime provisioning = ProvisioningRuntime.Create(
ProvisioningTestData.CreateOptions(),
ProvisioningTestData.CreateSecrets("secret-1"),
clock.UtcNow);
DedicatedPublisherPrincipal principal = ProvisioningTestData.CreateDedicatedPublisher();
string credential = provisioning.Credentials.Issue(principal, clock.UtcNow);
WebApplicationBuilder builder = WebApplication.CreateBuilder();
builder.WebHost.UseUrls("http://127.0.0.1:0");
builder.Services.ConfigureHttpJsonOptions(static options =>
ContractJson.Configure(options.SerializerOptions));
builder.Services.Configure<RouteHandlerOptions>(static options =>
options.ThrowOnBadRequest = true);
builder.Services.AddProblemDetails();
builder.Services.AddExceptionHandler<RendezvousExceptionHandler>();
builder.Services.AddSingleton(provisioning);
builder.Services.AddSingleton(provisioning.Credentials);
builder.Services.AddSingleton(provisioning.PublisherAuthorization);
builder.Services.AddSingleton<IEphemeralRendezvousStore>(store);
builder.Services.AddSingleton<IWallClock>(clock);
builder.Services.AddSingleton(capabilities);
builder.Services.AddSingleton<ISessionCapabilityService>(capabilities);
builder.Services.AddSingleton(SessionLeaseTiming.From(stateOptions));
builder.Services.AddSingleton<SessionLeaseService>();
builder.Services.AddSingleton<SessionBrowserCursorCodec>();
builder.Services.AddSingleton<SessionBrowserService>();
WebApplication app = builder.Build();
app.UseExceptionHandler();
app.MapRendezvousContractEndpoints();
await app.StartAsync();
IServer server = app.Services.GetRequiredService<IServer>();
string address = Assert.Single(server.Features.Get<IServerAddressesFeature>()!.Addresses);
return new(
app,
new HttpClient { BaseAddress = new Uri(address) },
store,
capabilities,
credential);
}
public async ValueTask DisposeAsync()
{
HttpClient.Dispose();
await _application.StopAsync();
await _application.DisposeAsync();
}
}
}
@@ -1,4 +1,5 @@
using System.Reflection;
using FinalFactory.Rendezvous.Client;
using FinalFactory.Rendezvous.Contracts;
namespace FinalFactory.Rendezvous.Tests.Contracts;
@@ -23,6 +24,24 @@ public sealed class PublicApiCompatibilityTests
Assert.Equal(expected, snapshot);
}
[Fact]
public void ClientPublicApiMatchesTheV1Snapshot()
{
string snapshot = CreateSnapshot(typeof(RendezvousPublisherClient).Assembly);
string expected = ContractTestFiles.Read("client-public-api.txt");
if (expected == "SNAPSHOT_PENDING"
&& Environment.GetEnvironmentVariable("RENDEZVOUS_UPDATE_CONTRACT_SNAPSHOT") == "1")
{
string snapshotPath = Path.Combine(
ContractTestFiles.Directory,
"client-public-api.txt");
File.WriteAllText(snapshotPath, snapshot + Environment.NewLine);
expected = snapshot;
}
Assert.Equal(expected, snapshot);
}
private static string CreateSnapshot(Assembly assembly)
{
List<string> lines = [];
@@ -58,10 +77,16 @@ public sealed class PublicApiCompatibilityTests
foreach (PropertyInfo property in type.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly)
.OrderBy(static property => property.Name, StringComparer.Ordinal))
{
string accessors = $"{(property.CanRead ? "get;" : string.Empty)}{(property.CanWrite ? "set;" : string.Empty)}";
string accessors = $"{(property.GetMethod?.IsPublic == true ? "get;" : string.Empty)}{(property.SetMethod?.IsPublic == true ? "set;" : string.Empty)}";
lines.Add($" PROP {FormatType(property.PropertyType)} {property.Name} {{{accessors}}}");
}
foreach (EventInfo eventInfo in type.GetEvents(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly)
.OrderBy(static eventInfo => eventInfo.Name, StringComparer.Ordinal))
{
lines.Add($" EVENT {FormatType(eventInfo.EventHandlerType!)} {eventInfo.Name}");
}
foreach (MethodInfo method in type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly)
.Where(static method => !method.IsSpecialName || method.Name.StartsWith("op_", StringComparison.Ordinal))
.OrderBy(static method => method.Name, StringComparer.Ordinal)
@@ -0,0 +1,60 @@
TYPE FinalFactory.Rendezvous.Client.IRendezvousDelay
METHOD System.Threading.Tasks.Task DelayAsync(System.TimeSpan delay, System.Threading.CancellationToken cancellationToken)
TYPE FinalFactory.Rendezvous.Client.IRendezvousPublisherClient
METHOD System.Threading.Tasks.Task<FinalFactory.Rendezvous.Client.RendezvousClientResult<System.Boolean>> DeregisterAsync(FinalFactory.Rendezvous.Client.PublishedSession session, System.String publisherCredential, System.Threading.CancellationToken cancellationToken)
METHOD System.Threading.Tasks.Task<FinalFactory.Rendezvous.Client.RendezvousClientResult<FinalFactory.Rendezvous.Client.PublishedSession>> RegisterAsync(FinalFactory.Rendezvous.Contracts.RegisterSessionRequest request, System.String publisherCredential, System.Threading.CancellationToken cancellationToken)
METHOD System.Threading.Tasks.Task<FinalFactory.Rendezvous.Client.RendezvousClientResult<FinalFactory.Rendezvous.Contracts.RenewLeaseResponse>> RenewAsync(FinalFactory.Rendezvous.Client.PublishedSession session, System.String publisherCredential, System.Threading.CancellationToken cancellationToken)
METHOD System.Threading.Tasks.Task<FinalFactory.Rendezvous.Client.RendezvousClientResult<System.Boolean>> UpdateAsync(FinalFactory.Rendezvous.Client.PublishedSession session, FinalFactory.Rendezvous.Contracts.UpdateSessionRequest request, System.String publisherCredential, System.Threading.CancellationToken cancellationToken)
TYPE FinalFactory.Rendezvous.Client.IRendezvousSessionBrowserClient
METHOD System.Threading.Tasks.Task<FinalFactory.Rendezvous.Client.RendezvousClientResult<System.Collections.Generic.IReadOnlyList<FinalFactory.Rendezvous.Contracts.SessionListing>>> BrowseAllAsync(FinalFactory.Rendezvous.Contracts.BrowseSessionsRequest request, System.Int32 maximumPages, System.Threading.CancellationToken cancellationToken)
METHOD System.Threading.Tasks.Task<FinalFactory.Rendezvous.Client.RendezvousClientResult<FinalFactory.Rendezvous.Contracts.BrowseSessionsResponse>> BrowseAsync(FinalFactory.Rendezvous.Contracts.BrowseSessionsRequest request, System.Threading.CancellationToken cancellationToken)
METHOD System.Threading.Tasks.Task<FinalFactory.Rendezvous.Client.RendezvousClientResult<FinalFactory.Rendezvous.Contracts.GetSessionResponse>> GetAsync(FinalFactory.Rendezvous.Contracts.SessionListingId listingId, FinalFactory.Rendezvous.Contracts.GameId gameId, FinalFactory.Rendezvous.Contracts.EnvironmentId environmentId, System.UInt32 protocolVersion, System.Threading.CancellationToken cancellationToken)
TYPE FinalFactory.Rendezvous.Client.LeaseMaintenanceResult
PROP FinalFactory.Rendezvous.Contracts.RendezvousErrorCode Error {get;}
PROP FinalFactory.Rendezvous.Client.LeaseMaintenanceStopReason Reason {get;}
TYPE FinalFactory.Rendezvous.Client.LeaseMaintenanceStopReason
ENUM Cancelled=1
ENUM Disposed=2
ENUM LeaseLost=3
ENUM Failed=4
TYPE FinalFactory.Rendezvous.Client.PublishedSession
PROP System.DateTimeOffset ExpiresAt {get;}
PROP System.String HostPresenceCapability {get;}
PROP FinalFactory.Rendezvous.Contracts.MediationHandle HostPresenceHandle {get;}
PROP System.Int32 HostPresenceRefreshAfterSeconds {get;}
PROP FinalFactory.Rendezvous.Contracts.LeaseId LeaseId {get;}
PROP System.Int32 LeaseRenewAfterSeconds {get;}
PROP System.String LeaseToken {get;}
PROP FinalFactory.Rendezvous.Contracts.SessionListingId ListingId {get;}
METHOD System.String ToString()
TYPE FinalFactory.Rendezvous.Client.RendezvousClientOptions
CTOR ()
PROP System.TimeSpan InitialRetryDelay {get;set;}
PROP System.Double JitterRatio {get;set;}
PROP System.TimeSpan MaximumRetryDelay {get;set;}
PROP System.Int32 MaximumSafeRetries {get;set;}
TYPE FinalFactory.Rendezvous.Client.RendezvousClientResult
METHOD FinalFactory.Rendezvous.Client.RendezvousClientResult<T> Failure(FinalFactory.Rendezvous.Contracts.RendezvousErrorCode error, System.String message, System.Nullable<System.Int32> retryAfterSeconds)
METHOD FinalFactory.Rendezvous.Client.RendezvousClientResult<T> Success(T value)
TYPE FinalFactory.Rendezvous.Client.RendezvousClientResult<T>
PROP FinalFactory.Rendezvous.Contracts.RendezvousErrorCode Error {get;}
PROP System.Boolean IsSuccess {get;}
PROP System.String Message {get;}
PROP System.Nullable<System.Int32> RetryAfterSeconds {get;}
PROP T Value {get;}
TYPE FinalFactory.Rendezvous.Client.RendezvousPublisherClient
CTOR (System.Net.Http.HttpClient httpClient, FinalFactory.Rendezvous.Client.RendezvousClientOptions options, FinalFactory.Rendezvous.Client.IRendezvousDelay delay)
METHOD FinalFactory.Rendezvous.Client.SessionLeaseMaintainer CreateLeaseMaintainer(FinalFactory.Rendezvous.Client.PublishedSession session, System.String publisherCredential)
METHOD System.Threading.Tasks.Task<FinalFactory.Rendezvous.Client.RendezvousClientResult<System.Boolean>> DeregisterAsync(FinalFactory.Rendezvous.Client.PublishedSession session, System.String publisherCredential, System.Threading.CancellationToken cancellationToken)
METHOD System.Threading.Tasks.Task<FinalFactory.Rendezvous.Client.RendezvousClientResult<FinalFactory.Rendezvous.Client.PublishedSession>> RegisterAsync(FinalFactory.Rendezvous.Contracts.RegisterSessionRequest request, System.String publisherCredential, System.Threading.CancellationToken cancellationToken)
METHOD System.Threading.Tasks.Task<FinalFactory.Rendezvous.Client.RendezvousClientResult<FinalFactory.Rendezvous.Contracts.RenewLeaseResponse>> RenewAsync(FinalFactory.Rendezvous.Client.PublishedSession session, System.String publisherCredential, System.Threading.CancellationToken cancellationToken)
METHOD System.Threading.Tasks.Task<FinalFactory.Rendezvous.Client.RendezvousClientResult<System.Boolean>> UpdateAsync(FinalFactory.Rendezvous.Client.PublishedSession session, FinalFactory.Rendezvous.Contracts.UpdateSessionRequest request, System.String publisherCredential, System.Threading.CancellationToken cancellationToken)
TYPE FinalFactory.Rendezvous.Client.RendezvousSessionBrowserClient
CTOR (System.Net.Http.HttpClient httpClient, FinalFactory.Rendezvous.Client.RendezvousClientOptions options, FinalFactory.Rendezvous.Client.IRendezvousDelay delay)
METHOD System.Threading.Tasks.Task<FinalFactory.Rendezvous.Client.RendezvousClientResult<System.Collections.Generic.IReadOnlyList<FinalFactory.Rendezvous.Contracts.SessionListing>>> BrowseAllAsync(FinalFactory.Rendezvous.Contracts.BrowseSessionsRequest request, System.Int32 maximumPages, System.Threading.CancellationToken cancellationToken)
METHOD System.Threading.Tasks.Task<FinalFactory.Rendezvous.Client.RendezvousClientResult<FinalFactory.Rendezvous.Contracts.BrowseSessionsResponse>> BrowseAsync(FinalFactory.Rendezvous.Contracts.BrowseSessionsRequest request, System.Threading.CancellationToken cancellationToken)
METHOD System.Threading.Tasks.Task<FinalFactory.Rendezvous.Client.RendezvousClientResult<FinalFactory.Rendezvous.Contracts.GetSessionResponse>> GetAsync(FinalFactory.Rendezvous.Contracts.SessionListingId listingId, FinalFactory.Rendezvous.Contracts.GameId gameId, FinalFactory.Rendezvous.Contracts.EnvironmentId environmentId, System.UInt32 protocolVersion, System.Threading.CancellationToken cancellationToken)
TYPE FinalFactory.Rendezvous.Client.SessionLeaseMaintainer
EVENT System.EventHandler LeaseLost
METHOD System.Threading.Tasks.ValueTask DisposeAsync()
METHOD System.Threading.Tasks.Task<FinalFactory.Rendezvous.Client.LeaseMaintenanceResult> RunAsync(System.Threading.CancellationToken cancellationToken)