using System.Text.Json; using System.Text.Json.Serialization; using ChessCubing.App.Models; namespace ChessCubing.App.Services; public sealed class MatchStore(BrowserBridge browser, UserSession userSession) { public const string StorageKeyPrefix = "chesscubing-arena-state-v3"; public const string WindowNameKeyPrefix = "chesscubing-arena-state-v3"; private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web) { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, }; private bool _dirty; private long _lastPersistedAt; private string? _activeStorageKey; private string? _activeWindowNameKey; public MatchState? Current { get; private set; } public bool IsLoaded { get; private set; } public async Task EnsureLoadedAsync() { var storageScope = await SyncStorageScopeAsync(); if (IsLoaded) { return; } try { var raw = await browser.ReadMatchJsonAsync(storageScope.StorageKey, storageScope.WindowNameKey); if (!string.IsNullOrWhiteSpace(raw)) { var parsed = JsonSerializer.Deserialize(raw, JsonOptions); if (parsed is not null && MatchEngine.IsSupportedSchemaVersion(parsed.SchemaVersion)) { MatchEngine.NormalizeRecoveredMatch(parsed); Current = parsed; } } } catch { Current = null; } IsLoaded = true; _lastPersistedAt = MatchEngine.NowUnixMs(); } public void SetCurrent(MatchState? match) { Current = match; MarkDirty(); } public void ReplaceCurrent(MatchState? match) { Current = match; IsLoaded = true; _dirty = false; } public void MarkDirty() => _dirty = true; public async Task SaveAsync() { var storageScope = await SyncStorageScopeAsync(); if (!IsLoaded) { return; } if (Current is null) { await browser.ClearMatchAsync(storageScope.StorageKey, storageScope.WindowNameKey); _dirty = false; _lastPersistedAt = MatchEngine.NowUnixMs(); return; } var json = JsonSerializer.Serialize(Current, JsonOptions); await browser.WriteMatchJsonAsync(storageScope.StorageKey, storageScope.WindowNameKey, json); _dirty = false; _lastPersistedAt = MatchEngine.NowUnixMs(); } public async Task FlushIfDueAsync(long minimumIntervalMs = 1_000) { if (!_dirty) { return; } if (MatchEngine.NowUnixMs() - _lastPersistedAt < minimumIntervalMs) { return; } await SaveAsync(); } public async Task ClearAsync() { var storageScope = await SyncStorageScopeAsync(); Current = null; IsLoaded = true; _dirty = false; _lastPersistedAt = MatchEngine.NowUnixMs(); await browser.ClearMatchAsync(storageScope.StorageKey, storageScope.WindowNameKey); } private async ValueTask SyncStorageScopeAsync() { var storageScope = await userSession.GetStorageScopeAsync(); if (_activeStorageKey is not null && (_activeStorageKey != storageScope.StorageKey || _activeWindowNameKey != storageScope.WindowNameKey)) { Current = null; IsLoaded = false; _dirty = false; } _activeStorageKey = storageScope.StorageKey; _activeWindowNameKey = storageScope.WindowNameKey; return storageScope; } }