feat: add presence-gated session leases (#7)
quality-gate / quality (push) Successful in 55s
quality-gate / quality (push) Successful in 55s
Closes #7
This commit is contained in:
@@ -53,6 +53,12 @@ internal sealed record EphemeralStoreOptions
|
||||
RequireDuration(ReplayLifetime, TimeSpan.FromSeconds(30), nameof(ReplayLifetime));
|
||||
RequireDuration(IdempotencyLifetime, TimeSpan.FromMinutes(10), nameof(IdempotencyLifetime));
|
||||
RequireDuration(GracefulDrainLifetime, TimeSpan.FromSeconds(30), nameof(GracefulDrainLifetime));
|
||||
if (IdempotencyLifetime < LeaseLifetime || IdempotencyLifetime < JoinAttemptLifetime)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(IdempotencyLifetime),
|
||||
"Idempotency retention must cover every idempotent resource lifetime.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void RequirePositive(int value, string name)
|
||||
@@ -74,8 +80,10 @@ internal sealed record EphemeralStoreOptions
|
||||
|
||||
internal readonly record struct TenantScope(GameId GameId, EnvironmentId EnvironmentId);
|
||||
|
||||
internal readonly record struct SecretFingerprint
|
||||
internal readonly struct SecretFingerprint : IEquatable<SecretFingerprint>
|
||||
{
|
||||
private readonly string? _value;
|
||||
|
||||
public SecretFingerprint(string value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value) || value.Length > 128)
|
||||
@@ -83,12 +91,33 @@ internal readonly record struct SecretFingerprint
|
||||
throw new ArgumentException("Secret fingerprints must contain 1-128 characters.", nameof(value));
|
||||
}
|
||||
|
||||
Value = value;
|
||||
_value = value;
|
||||
}
|
||||
|
||||
public string Value { get; }
|
||||
public bool IsValid => !string.IsNullOrWhiteSpace(Value) && Value.Length <= 128;
|
||||
public bool IsValid => !string.IsNullOrWhiteSpace(_value) && _value.Length <= 128;
|
||||
public bool Equals(SecretFingerprint other)
|
||||
{
|
||||
ReadOnlySpan<char> left = _value.AsSpan();
|
||||
ReadOnlySpan<char> right = other._value.AsSpan();
|
||||
if (left.Length != right.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int difference = 0;
|
||||
for (int index = 0; index < left.Length; index++)
|
||||
{
|
||||
difference |= left[index] ^ right[index];
|
||||
}
|
||||
|
||||
return difference == 0;
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj) => obj is SecretFingerprint other && Equals(other);
|
||||
public override int GetHashCode() => StringComparer.Ordinal.GetHashCode(_value ?? string.Empty);
|
||||
public override string ToString() => "[REDACTED]";
|
||||
public static bool operator ==(SecretFingerprint left, SecretFingerprint right) => left.Equals(right);
|
||||
public static bool operator !=(SecretFingerprint left, SecretFingerprint right) => !left.Equals(right);
|
||||
}
|
||||
|
||||
internal readonly record struct ObservedEndpoint
|
||||
@@ -138,6 +167,7 @@ internal sealed record ListingDefinition
|
||||
public required SecretFingerprint LeaseFingerprint { get; init; }
|
||||
public required MediationHandle HostPresenceHandle { get; init; }
|
||||
public required SecretFingerprint HostPresenceFingerprint { get; init; }
|
||||
public required string CapabilityDerivationSalt { get; init; }
|
||||
}
|
||||
|
||||
internal sealed record StoredListing
|
||||
@@ -163,12 +193,25 @@ internal sealed record RenewLeaseCommand(
|
||||
SessionListingId ListingId,
|
||||
LeaseId LeaseId,
|
||||
SecretFingerprint LeaseFingerprint,
|
||||
string OwnerSubject,
|
||||
long ExpectedVersion);
|
||||
|
||||
internal sealed record UpdateListingCommand(
|
||||
SessionListingId ListingId,
|
||||
LeaseId LeaseId,
|
||||
SecretFingerprint LeaseFingerprint,
|
||||
string OwnerSubject,
|
||||
string BuildVersion,
|
||||
string DisplayName,
|
||||
int CurrentPlayers,
|
||||
int MaximumPlayers,
|
||||
IReadOnlyDictionary<string, string> Metadata);
|
||||
|
||||
internal sealed record DeleteListingCommand(
|
||||
SessionListingId ListingId,
|
||||
LeaseId LeaseId,
|
||||
SecretFingerprint LeaseFingerprint);
|
||||
SecretFingerprint LeaseFingerprint,
|
||||
string OwnerSubject);
|
||||
|
||||
internal sealed record BindHostPresenceCommand(
|
||||
MediationHandle Handle,
|
||||
@@ -265,6 +308,7 @@ internal interface IEphemeralRendezvousStore
|
||||
|
||||
StoreResult<StoredListing> CreateListing(CreateListingCommand command, CancellationToken cancellationToken = default);
|
||||
StoreResult<StoredListing> RenewLease(RenewLeaseCommand command, CancellationToken cancellationToken = default);
|
||||
StoreResult<StoredListing> UpdateListing(UpdateListingCommand command, CancellationToken cancellationToken = default);
|
||||
StoreResult<bool> DeleteListing(DeleteListingCommand command, CancellationToken cancellationToken = default);
|
||||
StoreResult<StoredListing> GetListing(SessionListingId listingId, bool requireFreshPresence, CancellationToken cancellationToken = default);
|
||||
StoreResult<IReadOnlyList<StoredListing>> BrowseVisibleListings(VisibleListingQuery query, CancellationToken cancellationToken = default);
|
||||
|
||||
Reference in New Issue
Block a user