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); }