using decePubClient.Helpers; using decePubClient.Models; using PrivaPub.ClientModels; namespace decePubClient.Services { /// /// The IndexedDB database "data" (schema in GenericExtensions.AddIndexedDb): 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> 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 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 () => { if (!includeClientLogs) return "nothing to remove"; return await db.DeleteAll(nameof(ClientLogs)); }, 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); } } } }