using decePubClient.Helpers; using decePubClient.Models; using PrivaPub.ClientModels; namespace decePubClient.Services { /// /// The IndexedDB database "data" (schema in GenericExtensions.AddIndexedDb): messages and the client log. Lists /// come back empty on failure, mutations as an invalid WebResult; nothing throws. Failures are logged with ILogger /// only, because ILoggingService writes its log through this class. /// public interface IStorage { Task> GetMessages(CancellationToken cancellationToken = default); Task GetMessage(string messageId, CancellationToken cancellationToken = default); Task AddMessages(List messages, CancellationToken cancellationToken = default); Task UpdateMessages(List messages, CancellationToken cancellationToken = default); Task RemoveMessage(string messageId, CancellationToken cancellationToken = default); Task> GetClientLogs(CancellationToken cancellationToken = default); Task AddLog(Exception exception, string where, CancellationToken cancellationToken = default); Task AddLog(string message, string where, CancellationToken cancellationToken = default); Task RemoveAll(bool includeClientLogs = false, CancellationToken cancellationToken = default); } public class Storage(IndexedDb db, ILogger logger) : IStorage { /// The client log keeps at most this many entries; past it the oldest half is dropped. const int MaxLogs = 100; #region Messages public async Task GetMessage(string messageId, CancellationToken cancellationToken = default) { try { cancellationToken.ThrowIfCancellationRequested(); await db.OpenIndexedDb(); var message = await db.GetByKey(nameof(Message), messageId); if (message is not null) return message; var messages = await GetMessages(cancellationToken); return messages.FirstOrDefault(m => m.MessageId == messageId); } catch (Exception ex) { logger.LogError(ex, $"{nameof(Storage)}.{nameof(GetMessage)}"); return default; } } /// The stored messages, newest first; while none are stored, the mock feed (Faker.SeedMessages). public async Task> GetMessages(CancellationToken cancellationToken = default) { try { cancellationToken.ThrowIfCancellationRequested(); await db.OpenIndexedDb(); var messages = await db.GetAll(nameof(Message)) ?? []; if (messages.Count == 0) messages = Faker.SeedMessages(); return [.. messages.OrderByDescending(m => m.CreatedAt)]; } catch (Exception ex) { logger.LogError(ex, $"{nameof(Storage)}.{nameof(GetMessages)}"); return []; } } public Task AddMessages(List messages, CancellationToken cancellationToken = default) => Run(nameof(AddMessages), async () => await db.AddItems(nameof(Message), messages), cancellationToken); public Task UpdateMessages(List messages, CancellationToken cancellationToken = default) => Run(nameof(UpdateMessages), async () => await db.UpdateItems(nameof(Message), messages), cancellationToken); public Task RemoveMessage(string messageId, CancellationToken cancellationToken = default) => Run(nameof(RemoveMessage), async () => await db.DeleteByKey(nameof(Message), messageId), cancellationToken); #endregion #region Logs public async Task> GetClientLogs(CancellationToken cancellationToken = default) { try { cancellationToken.ThrowIfCancellationRequested(); await db.OpenIndexedDb(); var logs = await db.GetAll(nameof(ClientLogs)) ?? []; return [.. logs.OrderByDescending(log => log.TimeStamp)]; } catch (Exception ex) { logger.LogError(ex, $"{nameof(Storage)}.{nameof(GetClientLogs)}"); return []; } } public Task AddLog(Exception exception, string where, CancellationToken cancellationToken = default) => AddLog(new ClientLogs { Where = where, Exception = new() { Type = exception.GetType().ToString(), HelpLink = exception.HelpLink, HResult = exception.HResult.ToString(), InnerExceptionMessage = exception.InnerException?.Message, Message = exception.Message, Source = exception.Source, StackTrace = exception.StackTrace, TargetSiteName = exception.TargetSite?.Name } }, cancellationToken); public Task AddLog(string message, string where, CancellationToken cancellationToken = default) => AddLog(new ClientLogs { Where = where, WarningMessage = message }, cancellationToken); /// Numbers the entry after the newest one and trims the log; never throws. async Task AddLog(ClientLogs log, CancellationToken cancellationToken) { try { cancellationToken.ThrowIfCancellationRequested(); await db.OpenIndexedDb(); var maxKey = await db.GetMaxKey(nameof(ClientLogs)); var minKey = await db.GetMinKey(nameof(ClientLogs)); if (maxKey - minKey >= MaxLogs) for (var key = minKey; key < minKey + MaxLogs / 2; key++) _ = await db.DeleteByKey(nameof(ClientLogs), key); log.Id = maxKey + 1; var result = await db.AddItems(nameof(ClientLogs), [log]); logger.LogDebug($"{nameof(AddLog)}() add logs result = {result}"); } catch (Exception ex) { logger.LogError(ex, $"{nameof(Storage)}.{nameof(AddLog)}"); } } #endregion public Task RemoveAll(bool includeClientLogs = false, CancellationToken cancellationToken = default) => Run(nameof(RemoveAll), async () => { var result = await db.DeleteAll(nameof(Message)); if (includeClientLogs) result = await db.DeleteAll(nameof(ClientLogs)); return result; }, cancellationToken); /// Runs a write and turns its outcome into a WebResult. async Task Run(string caller, Func> write, CancellationToken cancellationToken) { var result = new WebResult(); try { cancellationToken.ThrowIfCancellationRequested(); await db.OpenIndexedDb(); logger.LogInformation("{Caller}: {Result}", caller, await write()); return result; } catch (OperationCanceledException) { return SUtility.DefaultTaskCancelledResult; } catch (Exception ex) { logger.LogError(ex, $"{nameof(Storage)}.{caller}"); return result.Invalidate(ex.Message, exception: ex); } } } }