using System.Net.Http.Json; using System.Text.Json; using EnvelopeGenerator.Application.Common.Dto; using Microsoft.Extensions.Logging; namespace EnvelopeGenerator.Server.Client.Services; /// /// Retrieves and persists user UI preferences via the server API. /// The server stores preferences in the distributed cache (dbo.TBDD_CACHE). /// /// Client-side memory cache: /// Preferences are cached in-memory for after the first fetch. /// All reads (GetAsync, GetCachedAsync) are served from cache after the first load. /// Writes (SaveAsync) update both the cache and the server atomically. /// Cache is invalidated explicitly via InvalidateCache(). /// public class UserPreferencesService( IHttpClientFactory clientFactory, ILogger logger) { private static readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web); private const string Endpoint = "/api/UserPreferences"; // ── Client-side memory cache ────────────────────────────────────────────── private static readonly TimeSpan CacheDuration = TimeSpan.FromHours(1); private UserPreferencesDto? _cached; private DateTimeOffset _cachedAt = DateTimeOffset.MinValue; private bool IsCacheValid => _cached is not null && DateTimeOffset.UtcNow - _cachedAt < CacheDuration; /// /// Returns preferences from in-memory cache if valid; otherwise fetches from the server. /// On first call after login this issues one HTTP GET — all subsequent calls within /// are served from memory with zero network overhead. /// public async Task GetAsync(CancellationToken ct = default) { if (IsCacheValid) return _cached!; try { var client = clientFactory.CreateClient("EnvelopeGenerator.Server"); var response = await client.GetAsync(Endpoint, ct); // 404 means no preferences saved yet — return and cache defaults. if (response.StatusCode == System.Net.HttpStatusCode.NotFound) { SetCache(new UserPreferencesDto()); return _cached!; } response.EnsureSuccessStatusCode(); var dto = await response.Content.ReadFromJsonAsync(_jsonOptions, ct) ?? new UserPreferencesDto(); SetCache(dto); return _cached!; } catch (Exception ex) when (ex is not TaskCanceledException) { logger.LogError(ex, "Failed to load user preferences from server."); throw; } } /// /// Returns the cached preferences without hitting the network. /// Returns null if the cache has not been populated yet (before first GetAsync call). /// public UserPreferencesDto? GetCached() => IsCacheValid ? _cached : null; /// /// Persists the given preferences on the server and updates the in-memory cache. /// public async Task SaveAsync(UserPreferencesDto preferences, CancellationToken ct = default) { try { var client = clientFactory.CreateClient("EnvelopeGenerator.Server"); var response = await client.PutAsJsonAsync(Endpoint, preferences, _jsonOptions, ct); response.EnsureSuccessStatusCode(); var saved = await response.Content.ReadFromJsonAsync(_jsonOptions, ct) ?? preferences; SetCache(saved); return saved; } catch (Exception ex) when (ex is not TaskCanceledException) { logger.LogError(ex, "Failed to save user preferences to server."); throw; } } /// /// Updates a single grid layout key without a network read. /// Uses the in-memory cache for the current state, merges the new layout, then saves. /// If the cache is empty (cold start), fetches from server first. /// /// Stable grid identifier, e.g. "sender.active-envelopes". /// Opaque layout JSON produced by DxGrid.SaveLayoutAsync(). public async Task SaveGridLayoutAsync(string gridKey, string layoutJson, CancellationToken ct = default) { // Use cached copy — avoids a GET before every grid layout save. var prefs = GetCached() ?? await GetAsync(ct); prefs.GridLayouts[gridKey] = layoutJson; await SaveAsync(prefs, ct); } /// /// Invalidates the in-memory cache, forcing the next GetAsync to fetch from the server. /// Call this after logout or when a forced refresh is needed. /// public void InvalidateCache() { _cached = null; _cachedAt = DateTimeOffset.MinValue; } private void SetCache(UserPreferencesDto dto) { _cached = dto; _cachedAt = DateTimeOffset.UtcNow; } }