< Summary

Information
Class: Pomodoro.Web.Services.IndexedDbService
Assembly: Pomodoro.Web
File(s): /home/runner/work/Pomodoro/Pomodoro/src/Pomodoro.Web/Services/IndexedDbService.cs
Line coverage
100%
Covered lines: 171
Uncovered lines: 0
Coverable lines: 171
Total lines: 297
Line coverage: 100%
Branch coverage
100%
Covered branches: 28
Total branches: 28
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
InitializeAsync()100%22100%
InitializeJsConstantsAsync()100%11100%
GetAsync()100%44100%
GetAllAsync()100%44100%
QueryByIndexAsync()100%44100%
QueryByDateRangeAsync()100%44100%
PutAsync()100%11100%
PutAllAsync()100%44100%
DeleteAsync()100%11100%
ClearAsync()100%11100%
GetCountAsync()100%11100%
DisposeAsync()100%11100%
EnsureInitialized()100%22100%
NotifyStorageError(...)100%22100%
DeserializeItem(...)100%11100%
DeserializeList(...)100%22100%
HandleDeserializationError(...)100%11100%

File(s)

/home/runner/work/Pomodoro/Pomodoro/src/Pomodoro.Web/Services/IndexedDbService.cs

#LineLine coverage
 1using System.Text.Json;
 2using Microsoft.JSInterop;
 3using Microsoft.Extensions.Logging;
 4
 5namespace Pomodoro.Web.Services;
 6
 7/// <summary>
 8/// Service for IndexedDB operations via JavaScript interop
 9/// Provides persistent storage with larger capacity than localStorage
 10/// </summary>
 11public class IndexedDbService : IIndexedDbService, IAsyncDisposable
 12{
 13    private readonly IJSRuntime _jsRuntime;
 14    private readonly JsonSerializerOptions _jsonOptions;
 15    private readonly ILogger<IndexedDbService> _logger;
 16    private bool _isInitialized;
 17
 18    /// <summary>
 19    /// Event raised when a storage error occurs
 20    /// </summary>
 21    public event Action<string>? OnStorageError;
 22
 38323    public IndexedDbService(IJSRuntime jsRuntime, ILogger<IndexedDbService> logger)
 38324    {
 38325        _jsRuntime = jsRuntime;
 38326        _logger = logger;
 38327        _jsonOptions = new JsonSerializerOptions
 38328        {
 38329            PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
 38330            PropertyNameCaseInsensitive = true
 38331        };
 38332    }
 33
 34    public async Task InitializeAsync()
 33135    {
 34336        if (_isInitialized) return;
 37
 38        try
 31939        {
 40            // The indexedDbInterop.js is loaded via script tag in index.html, so we use _jsRuntime directly
 41            // rather than importing as ES module (the JS uses window.indexedDbInterop pattern)
 31942            await _jsRuntime.InvokeVoidAsync(Constants.IndexedDbJsFunctions.InitDatabase);
 30743            _isInitialized = true;
 30744            _logger.LogDebug(Constants.Messages.LogInitializedSuccessfully);
 30745        }
 1246        catch (Exception ex)
 1247        {
 1248            _logger.LogError(ex, Constants.Messages.LogFailedToInitialize);
 1249            throw;
 50        }
 31951    }
 52
 53    /// <summary>
 54    /// Initializes JavaScript constants with user settings for consistency across JS interop
 55    /// </summary>
 56    public async Task InitializeJsConstantsAsync(int pomodoroMinutes, int shortBreakMinutes, int longBreakMinutes)
 2457    {
 58        try
 2459        {
 2460            await _jsRuntime.InvokeVoidAsync(Constants.IndexedDbJsFunctions.PomodoroConstantsInitialize, new
 2461            {
 2462                pomodoroMinutes,
 2463                shortBreakMinutes,
 2464                longBreakMinutes
 2465            });
 1266            _logger.LogDebug(Constants.Messages.LogJsConstantsInitializedFormat,
 1267                pomodoroMinutes, shortBreakMinutes, longBreakMinutes);
 1268        }
 1269        catch (Exception ex)
 1270        {
 1271            _logger.LogWarning(ex, Constants.Messages.LogFailedToInitializeJsConstants);
 1272        }
 2473    }
 74
 75    public async Task<T?> GetAsync<T>(string storeName, string key)
 6176    {
 6177        EnsureInitialized();
 78
 79        try
 4980        {
 4981            var result = await _jsRuntime.InvokeAsync<JsonElement>(Constants.IndexedDbJsFunctions.GetItem, storeName, ke
 3782            if (result.ValueKind == JsonValueKind.Null || result.ValueKind == JsonValueKind.Undefined)
 1283                return default;
 84
 2585            return DeserializeItem<T>(result, storeName, key);
 86        }
 1287        catch (Exception ex)
 1288        {
 89            // Log error for debugging - storage failures should be investigated
 1290            _logger.LogError(ex, Constants.Messages.LogErrorGettingItem, storeName, key);
 91            // Notify subscribers of the error
 1292            NotifyStorageError(string.Format(Constants.Messages.LogFailedToGetItem, storeName, ex.Message));
 1293            return default;
 94        }
 4995    }
 96
 97    public async Task<List<T>> GetAllAsync<T>(string storeName)
 3298    {
 3299        EnsureInitialized();
 100
 101        try
 31102        {
 31103            var result = await _jsRuntime.InvokeAsync<JsonElement>(Constants.IndexedDbJsFunctions.GetAllItems, storeName
 18104            if (result.ValueKind == JsonValueKind.Null || result.ValueKind == JsonValueKind.Undefined)
 13105                return new List<T>();
 106
 5107            return DeserializeList<T>(result, storeName, "all");
 108        }
 13109        catch (Exception ex)
 13110        {
 111            // Log error for debugging - storage failures should be investigated
 13112            _logger.LogError(ex, Constants.Messages.LogErrorGettingAllItems, storeName);
 113            // Notify subscribers of the error
 13114            NotifyStorageError(string.Format(Constants.Messages.LogFailedToGetAllItems, storeName, ex.Message));
 13115            return new List<T>();
 116        }
 31117    }
 118
 119    public async Task<List<T>> QueryByIndexAsync<T>(string storeName, string indexName, object value)
 38120    {
 38121        EnsureInitialized();
 122
 123        try
 38124        {
 38125            var result = await _jsRuntime.InvokeAsync<JsonElement>(Constants.IndexedDbJsFunctions.GetItemsByIndex, store
 26126            if (result.ValueKind == JsonValueKind.Null || result.ValueKind == JsonValueKind.Undefined)
 13127                return new List<T>();
 128
 13129            return DeserializeList<T>(result, storeName, indexName);
 130        }
 12131        catch (Exception ex)
 12132        {
 12133            _logger.LogDebug(ex, Constants.Messages.LogErrorQueryingIndex, indexName);
 12134            return new List<T>();
 135        }
 38136    }
 137
 138    public async Task<List<T>> QueryByDateRangeAsync<T>(string storeName, string indexName, string startDate, string end
 39139    {
 39140        EnsureInitialized();
 141
 142        try
 39143        {
 39144            var result = await _jsRuntime.InvokeAsync<JsonElement>(Constants.IndexedDbJsFunctions.GetItemsByDateRange, s
 26145            if (result.ValueKind == JsonValueKind.Null || result.ValueKind == JsonValueKind.Undefined)
 13146                return new List<T>();
 147
 13148            return DeserializeList<T>(result, storeName, indexName);
 149        }
 13150        catch (Exception ex)
 13151        {
 13152            _logger.LogDebug(ex, Constants.Messages.LogErrorQueryingDateRange);
 13153            return new List<T>();
 154        }
 39155    }
 156
 157    public async Task<bool> PutAsync<T>(string storeName, T item)
 24158    {
 24159        EnsureInitialized();
 160
 161        try
 24162        {
 24163            await _jsRuntime.InvokeVoidAsync(Constants.IndexedDbJsFunctions.PutItem, storeName, item);
 12164            return true;
 165        }
 12166        catch (Exception ex)
 12167        {
 12168            _logger.LogError(ex, Constants.Messages.LogErrorPuttingItem, storeName);
 12169            return false;
 170        }
 24171    }
 172
 173    /// <summary>
 174    /// Puts all items in a single transaction for better performance.
 175    /// Note: Uses a single transaction for atomicity - if one item fails, all changes are rolled back.
 176    /// </summary>
 177    public async Task<bool> PutAllAsync<T>(string storeName, List<T> items)
 51178    {
 51179        EnsureInitialized();
 180
 51181        if (items == null || items.Count == 0)
 27182            return true;
 183
 184        try
 24185        {
 24186            await _jsRuntime.InvokeVoidAsync(Constants.IndexedDbJsFunctions.PutAllItems, storeName, items);
 12187            return true;
 188        }
 12189        catch (Exception ex)
 12190        {
 12191            _logger.LogError(ex, Constants.Messages.LogErrorPuttingAllItems, storeName);
 12192            return false;
 193        }
 51194    }
 195
 196    public async Task<bool> DeleteAsync(string storeName, string key)
 24197    {
 24198        EnsureInitialized();
 199
 200        try
 24201        {
 24202            await _jsRuntime.InvokeVoidAsync(Constants.IndexedDbJsFunctions.DeleteItem, storeName, key);
 12203            return true;
 204        }
 12205        catch (Exception ex)
 12206        {
 12207            _logger.LogError(ex, Constants.Messages.LogErrorDeletingItem, storeName);
 12208            return false;
 209        }
 24210    }
 211
 212    public async Task<bool> ClearAsync(string storeName)
 25213    {
 25214        EnsureInitialized();
 215
 216        try
 25217        {
 25218            await _jsRuntime.InvokeVoidAsync(Constants.IndexedDbJsFunctions.ClearStore, storeName);
 13219            return true;
 220        }
 12221        catch (Exception ex)
 12222        {
 12223            _logger.LogError(ex, Constants.Messages.LogErrorClearingStore, storeName);
 12224            return false;
 225        }
 25226    }
 227
 228    public async Task<int> GetCountAsync(string storeName)
 3229    {
 3230        EnsureInitialized();
 231
 232        try
 2233        {
 2234            return await _jsRuntime.InvokeAsync<int>(Constants.IndexedDbJsFunctions.GetCount, storeName);
 235        }
 1236        catch (Exception ex)
 1237        {
 1238            _logger.LogDebug(ex, Constants.Messages.LogErrorGettingCount, storeName);
 1239            return 0;
 240        }
 2241    }
 242
 243    public ValueTask DisposeAsync()
 12244    {
 245        // No module to dispose since we're using _jsRuntime directly
 12246        return ValueTask.CompletedTask;
 12247    }
 248
 249    private void EnsureInitialized()
 297250    {
 297251        if (!_isInitialized)
 14252        {
 14253            throw new InvalidOperationException(Constants.Messages.IndexedDbNotInitialized);
 254        }
 283255    }
 256
 257    /// <summary>
 258    /// Notifies subscribers of storage errors
 259    /// </summary>
 260    private void NotifyStorageError(string errorMessage)
 44261    {
 44262        OnStorageError?.Invoke(errorMessage);
 44263    }
 264
 265    private T? DeserializeItem<T>(JsonElement result, string storeName, string key)
 25266    {
 267        try
 25268        {
 25269            return JsonSerializer.Deserialize<T>(result.GetRawText(), _jsonOptions);
 270        }
 13271        catch (JsonException jsonEx)
 13272        {
 13273            HandleDeserializationError(jsonEx, storeName, key);
 13274            return default;
 275        }
 25276    }
 277
 278    private List<T> DeserializeList<T>(JsonElement result, string storeName, string key)
 43279    {
 280        try
 43281        {
 43282            var list = JsonSerializer.Deserialize<List<T>>(result.GetRawText(), _jsonOptions);
 37283            return list ?? new List<T>();
 284        }
 6285        catch (JsonException jsonEx)
 6286        {
 6287            HandleDeserializationError(jsonEx, storeName, key);
 6288            return new List<T>();
 289        }
 43290    }
 291
 292    private void HandleDeserializationError(JsonException jsonEx, string storeName, string key)
 19293    {
 19294        _logger.LogWarning(jsonEx, Constants.Messages.LogJsonDeserializationFailed, storeName, key);
 19295        NotifyStorageError($"Data corruption detected in {storeName}");
 19296    }
 297}