using ZiggyCreatures.Caching.Fusion;
namespace LANCommander.Server.Services;
///
/// Tracks the last time each active play session was heard from via the RPC keepalive. Backed by
/// so it works across instances when a distributed (e.g. Redis) backend
/// is configured. The sweep consults this to end sessions that have gone stale, ending them at the
/// last known-alive time so recorded playtime isn't inflated.
///
public sealed class PlaySessionKeepAliveTracker(
IFusionCache cache,
SettingsProvider settingsProvider)
{
private static string GetCacheKey(Guid gameId, Guid userId) => $"PlaySessions/KeepAlive/{gameId}/{userId}";
// Keep entries alive well past the staleness timeout so the sweep can still read the last
// keepalive time when ending a stale session, rather than losing it to expiration.
private TimeSpan EntryDuration =>
TimeSpan.FromSeconds(Math.Max(1, settingsProvider.CurrentValue.Server.GameServers.KeepAliveTimeout) * 3);
/// Records that the session for this game/user was heard from just now.
public async Task TouchAsync(Guid gameId, Guid userId) =>
await cache.SetAsync(GetCacheKey(gameId, userId), DateTime.UtcNow, EntryDuration);
///
/// Returns the last-seen time for the session, seeding it with the current time if we have no
/// record yet (e.g. a session that predates a restart with no distributed backend). Seeding
/// grants a fresh grace window rather than immediately reaping a session we haven't observed yet.
///
public async Task GetOrSeedAsync(Guid gameId, Guid userId)
{
var cacheKey = GetCacheKey(gameId, userId);
var lastSeen = await cache.TryGetAsync(cacheKey);
if (lastSeen.HasValue)
return lastSeen.Value;
var now = DateTime.UtcNow;
await cache.SetAsync(cacheKey, now, EntryDuration);
return now;
}
/// Stops tracking the session (it ended normally or was swept).
public async Task RemoveAsync(Guid gameId, Guid userId) =>
await cache.RemoveAsync(GetCacheKey(gameId, userId));
}