namespace Elternbeirat.Web.Shared;
///
/// Remembers for a few seconds whether PocketBase answered its health check, so the
/// maintenance gate does not probe PocketBase on every page request.
///
///
/// The clock that decides when a result is stale; injected so tests can step past
/// without waiting.
///
///
/// The gate in Program.cs asks this cache before rendering any page and serves
/// the static maintenance.html with 503 while the answer is "unhealthy". A
/// failed check is kept just as long as a passed one: while PocketBase is down every
/// probe would otherwise wait for the connection to fail again.
///
/// The price of caching is a short blind spot in both directions: after
/// PocketBase fails, pages may still render for up to (the
/// components' own fallbacks cover that), and after it recovers the maintenance
/// page may stay up for as long.
///
///
/// Registered as a singleton. No lock: two requests that find the result stale at
/// the same moment both probe, and the later answer wins -- harmless, and cheaper
/// than making every request wait on one another.
///
///
///
///
/// var healthy = await cache.IsHealthyAsync(pocketBase.IsHealthyAsync, context.RequestAborted);
///
///
///
public sealed class PocketBaseHealthCache(TimeProvider time)
{
///
/// How long a health check result is reused before PocketBase is asked again.
///
public static readonly TimeSpan Lifetime = TimeSpan.FromSeconds(10);
// One reference, swapped as a whole, so a reader never sees the result of one
// check paired with the time of another.
private volatile Check? _last;
///
/// Returns the last known health of PocketBase, probing again once it is older
/// than .
///
///
/// The actual check, usually .
/// Passed in rather than injected because the typed client is transient and this
/// cache is a singleton.
///
/// Cancels the probe, e.g. when the visitor goes away.
///
/// if PocketBase answered its last check (or answers this
/// one), otherwise .
///
///
/// was cancelled during a probe; nothing is cached then.
///
public async Task IsHealthyAsync(Func> probe, CancellationToken token) =>
_last is { } last && time.GetUtcNow() - last.At < Lifetime
? last.Healthy
: (_last = new Check(await probe(token), time.GetUtcNow())).Healthy;
///
/// One health check result and when it was taken.
///
/// Whether PocketBase answered.
/// When the answer came in.
private sealed record Check(bool Healthy, DateTimeOffset At);
}