/ src / modules / cmdpal / Microsoft.CmdPal.UI.ViewModels / Services / DefaultCommandProviderCache.cs
DefaultCommandProviderCache.cs
1 // Copyright (c) Microsoft Corporation 2 // The Microsoft Corporation licenses this file to you under the MIT license. 3 // See the LICENSE file in the project root for more information. 4 5 using System.Text.Json; 6 using ManagedCommon; 7 using Microsoft.CmdPal.Core.Common.Helpers; 8 using Microsoft.CommandPalette.Extensions.Toolkit; 9 10 namespace Microsoft.CmdPal.UI.ViewModels.Services; 11 12 public sealed partial class DefaultCommandProviderCache : ICommandProviderCache, IDisposable 13 { 14 private const string CacheFileName = "commandProviderCache.json"; 15 16 private readonly Dictionary<string, CommandProviderCacheItem> _cache = new(StringComparer.Ordinal); 17 18 private readonly Lock _sync = new(); 19 20 private readonly SupersedingAsyncGate _saveGate; 21 22 public DefaultCommandProviderCache() 23 { 24 _saveGate = new SupersedingAsyncGate(async _ => await TrySaveAsync().ConfigureAwait(false)); 25 TryLoad(); 26 } 27 28 public void Memorize(string providerId, CommandProviderCacheItem item) 29 { 30 ArgumentNullException.ThrowIfNull(providerId); 31 32 lock (_sync) 33 { 34 _cache[providerId] = item; 35 } 36 37 _ = _saveGate.ExecuteAsync(); 38 } 39 40 public CommandProviderCacheItem? Recall(string providerId) 41 { 42 ArgumentNullException.ThrowIfNull(providerId); 43 44 lock (_sync) 45 { 46 _cache.TryGetValue(providerId, out var item); 47 return item; 48 } 49 } 50 51 private static string GetCacheFilePath() 52 { 53 var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal"); 54 Directory.CreateDirectory(directory); 55 return Path.Combine(directory, CacheFileName); 56 } 57 58 private void TryLoad() 59 { 60 try 61 { 62 var path = GetCacheFilePath(); 63 if (!File.Exists(path)) 64 { 65 return; 66 } 67 68 var json = File.ReadAllText(path); 69 if (string.IsNullOrWhiteSpace(json)) 70 { 71 return; 72 } 73 74 var loaded = JsonSerializer.Deserialize( 75 json, 76 CommandProviderCacheSerializationContext.Default.CommandProviderCacheContainer!); 77 if (loaded?.Cache is null) 78 { 79 return; 80 } 81 82 _cache.Clear(); 83 foreach (var kvp in loaded.Cache) 84 { 85 if (!string.IsNullOrEmpty(kvp.Key) && kvp.Value is not null) 86 { 87 _cache[kvp.Key] = kvp.Value; 88 } 89 } 90 } 91 catch (Exception ex) 92 { 93 Logger.LogError("Failed to load command provider cache: ", ex); 94 } 95 } 96 97 private async Task TrySaveAsync() 98 { 99 try 100 { 101 Dictionary<string, CommandProviderCacheItem> snapshot; 102 lock (_sync) 103 { 104 snapshot = new Dictionary<string, CommandProviderCacheItem>(_cache, StringComparer.Ordinal); 105 } 106 107 var container = new CommandProviderCacheContainer 108 { 109 Cache = snapshot, 110 }; 111 112 var path = GetCacheFilePath(); 113 var json = JsonSerializer.Serialize(container, CommandProviderCacheSerializationContext.Default.CommandProviderCacheContainer!); 114 await File.WriteAllTextAsync(path, json).ConfigureAwait(false); 115 } 116 catch (Exception ex) 117 { 118 Logger.LogError("Failed to save command provider cache: ", ex); 119 } 120 } 121 122 public void Dispose() 123 { 124 _saveGate.Dispose(); 125 GC.SuppressFinalize(this); 126 } 127 }