feat: add presence-gated session leases (#7)
quality-gate / quality (push) Successful in 55s

Closes #7
This commit is contained in:
KyuubiYoru
2026-07-16 05:58:47 +02:00
parent 02ca502a76
commit 49564c7e7e
25 changed files with 2069 additions and 46 deletions
@@ -80,7 +80,7 @@ internal sealed class InMemoryEphemeralRendezvousStore : IEphemeralRendezvousSto
return admission;
}
string idempotencyKey = $"listing:{command.Listing.OwnerSubject}:{command.IdempotencyKey}";
string idempotencyKey = $"listing:{command.Listing.Scope.GameId}:{command.Listing.Scope.EnvironmentId}:{command.Listing.OwnerSubject}:{command.IdempotencyKey}";
if (_idempotency.TryGetValue(idempotencyKey, out IdempotencyEntry? previous))
{
if (!string.Equals(previous.RequestFingerprint, command.RequestFingerprint, StringComparison.Ordinal))
@@ -151,7 +151,8 @@ internal sealed class InMemoryEphemeralRendezvousStore : IEphemeralRendezvousSto
}
if (entry.Definition.LeaseId != command.LeaseId
|| entry.Definition.LeaseFingerprint != command.LeaseFingerprint)
|| entry.Definition.LeaseFingerprint != command.LeaseFingerprint
|| !string.Equals(entry.Definition.OwnerSubject, command.OwnerSubject, StringComparison.Ordinal))
{
return new(StoreResultCode.NotFound);
}
@@ -167,6 +168,52 @@ internal sealed class InMemoryEphemeralRendezvousStore : IEphemeralRendezvousSto
return new(StoreResultCode.Success, Snapshot(entry));
}, cancellationToken);
public StoreResult<StoredListing> UpdateListing(
UpdateListingCommand command,
CancellationToken cancellationToken = default) => Atomic<StoredListing>(_ =>
{
ArgumentNullException.ThrowIfNull(command);
ValidateSubject(command.OwnerSubject, nameof(command.OwnerSubject));
if (!ContractValidation.IsBuildVersionValid(command.BuildVersion)
|| !ContractValidation.IsDisplayNameValid(command.DisplayName)
|| command.MaximumPlayers is <= 0 or > ContractLimits.SessionCapacityMaxPlayers
|| command.CurrentPlayers < 0
|| command.CurrentPlayers > command.MaximumPlayers
|| !ContractValidation.IsMetadataValid(command.Metadata))
{
throw new ArgumentException("Listing update invariants are invalid.", nameof(command));
}
if (!_available)
{
return new(StoreResultCode.ServiceUnavailable);
}
if (_drainDeadline.HasValue)
{
return new(StoreResultCode.Draining);
}
if (!_listings.TryGetValue(command.ListingId, out ListingEntry? entry)
|| entry.Definition.LeaseId != command.LeaseId
|| entry.Definition.LeaseFingerprint != command.LeaseFingerprint
|| !string.Equals(entry.Definition.OwnerSubject, command.OwnerSubject, StringComparison.Ordinal))
{
return new(StoreResultCode.NotFound);
}
entry.Definition = StoredListing.Freeze(entry.Definition with
{
BuildVersion = command.BuildVersion,
DisplayName = command.DisplayName,
CurrentPlayers = command.CurrentPlayers,
MaximumPlayers = command.MaximumPlayers,
Metadata = command.Metadata,
});
entry.Version++;
return new(StoreResultCode.Success, Snapshot(entry));
}, cancellationToken);
public StoreResult<bool> DeleteListing(
DeleteListingCommand command,
CancellationToken cancellationToken = default) => Atomic<bool>(_ =>
@@ -174,7 +221,8 @@ internal sealed class InMemoryEphemeralRendezvousStore : IEphemeralRendezvousSto
ArgumentNullException.ThrowIfNull(command);
if (!_listings.TryGetValue(command.ListingId, out ListingEntry? entry)
|| entry.Definition.LeaseId != command.LeaseId
|| entry.Definition.LeaseFingerprint != command.LeaseFingerprint)
|| entry.Definition.LeaseFingerprint != command.LeaseFingerprint
|| !string.Equals(entry.Definition.OwnerSubject, command.OwnerSubject, StringComparison.Ordinal))
{
return new(StoreResultCode.NotFound);
}
@@ -287,7 +335,7 @@ internal sealed class InMemoryEphemeralRendezvousStore : IEphemeralRendezvousSto
return admission;
}
string idempotencyKey = $"attempt:{command.IdempotencyOwner}:{command.IdempotencyKey}";
string idempotencyKey = $"attempt:{command.Scope.GameId}:{command.Scope.EnvironmentId}:{command.IdempotencyOwner}:{command.IdempotencyKey}";
if (_idempotency.TryGetValue(idempotencyKey, out IdempotencyEntry? previous))
{
if (!string.Equals(previous.RequestFingerprint, command.RequestFingerprint, StringComparison.Ordinal))
@@ -699,7 +747,8 @@ internal sealed class InMemoryEphemeralRendezvousStore : IEphemeralRendezvousSto
|| listing.CurrentPlayers > listing.MaximumPlayers
|| !ContractValidation.IsMetadataValid(listing.Metadata)
|| !listing.LeaseFingerprint.IsValid
|| !listing.HostPresenceFingerprint.IsValid)
|| !listing.HostPresenceFingerprint.IsValid
|| !IsDerivationSaltValid(listing.CapabilityDerivationSalt))
{
throw new ArgumentException("Listing invariants are invalid.", nameof(listing));
}
@@ -753,6 +802,15 @@ internal sealed class InMemoryEphemeralRendezvousStore : IEphemeralRendezvousSto
private static bool IsScopeValid(TenantScope scope) =>
!string.IsNullOrEmpty(scope.GameId.Value) && !string.IsNullOrEmpty(scope.EnvironmentId.Value);
private static bool IsDerivationSaltValid(string? value) => value is not null
&& value.Length == 43
&& value.All(static character =>
character is >= 'A' and <= 'Z'
or >= 'a' and <= 'z'
or >= '0' and <= '9'
or '-'
or '_');
private static void ValidateSubject(string subject, string parameterName)
{
if (string.IsNullOrWhiteSpace(subject) || subject.Length > 256)
@@ -767,7 +825,7 @@ internal sealed class InMemoryEphemeralRendezvousStore : IEphemeralRendezvousSto
DateTimeOffset wallExpiresAt,
long version)
{
public ListingDefinition Definition { get; } = definition;
public ListingDefinition Definition { get; set; } = definition;
public TimeSpan LeaseDeadline { get; set; } = leaseDeadline;
public DateTimeOffset WallExpiresAt { get; set; } = wallExpiresAt;
public long Version { get; set; } = version;