@@ -7,40 +7,100 @@ namespace ktsu.CredentialCache;
77using System . Collections . Concurrent ;
88using System . Text . Json . Serialization ;
99
10- using ktsu . AppDataStorage ;
1110using ktsu . Extensions ;
11+ using ktsu . FileSystemProvider ;
12+ using ktsu . PersistenceProvider ;
1213using ktsu . StrongStrings ;
14+ using ktsu . UniversalSerializer . Json ;
15+ using ktsu . UniversalSerializer . Services ;
1316
1417/// <summary>
1518/// Represents a globally unique identifier for a persona.
1619/// </summary>
1720public sealed record class PersonaGUID : StrongStringAbstract < PersonaGUID > { }
1821
22+ /// <summary>
23+ /// Data model for credential cache persistence.
24+ /// </summary>
25+ internal sealed class CredentialCacheData
26+ {
27+ /// <summary>
28+ /// Gets or sets the dictionary of credentials.
29+ /// </summary>
30+ [ JsonInclude ]
31+ public ConcurrentDictionary < PersonaGUID , Credential > Credentials { get ; set ; } = new ( ) ;
32+ }
33+
1934/// <summary>
2035/// Manages the caching of credentials and their associated factories.
2136/// </summary>
22- public class CredentialCache : AppData < CredentialCache >
37+ public sealed class CredentialCache : IDisposable
2338{
39+ private const string CacheKey = "CredentialCache" ;
40+ private static readonly object _lock = new ( ) ;
41+ private static CredentialCache ? _instance ;
42+ private static IPersistenceProvider < string > ? _persistenceProvider ;
43+
2444 /// <summary>
2545 /// Gets the dictionary of credential factories.
2646 /// </summary>
27- private ConcurrentDictionary < Type , ICredentialFactory > CredentialFactories { get ; init ; } = new ( ) ;
47+ private ConcurrentDictionary < Type , ICredentialFactory > CredentialFactories { get ; } = new ( ) ;
2848
2949 /// <summary>
30- /// Gets the dictionary of credentials .
50+ /// Gets the underlying data model .
3151 /// </summary>
32- [ JsonInclude ]
33- private ConcurrentDictionary < PersonaGUID , Credential > Credentials { get ; init ; } = new ( ) ;
52+ private CredentialCacheData Data { get ; set ; }
53+
54+ /// <summary>
55+ /// Gets the persistence provider used for storage operations.
56+ /// </summary>
57+ private IPersistenceProvider < string > PersistenceProvider { get ; }
58+
59+ /// <summary>
60+ /// Initializes a new instance of the <see cref="CredentialCache"/> class.
61+ /// </summary>
62+ /// <param name="persistenceProvider">The persistence provider for storage operations.</param>
63+ private CredentialCache ( IPersistenceProvider < string > persistenceProvider )
64+ {
65+ PersistenceProvider = persistenceProvider ?? throw new ArgumentNullException ( nameof ( persistenceProvider ) ) ;
66+ Data = LoadOrCreateData ( ) . GetAwaiter ( ) . GetResult ( ) ;
67+ }
3468
3569 /// <summary>
3670 /// Gets the singleton instance of the <see cref="CredentialCache"/> class.
3771 /// </summary>
38- private static Lazy < CredentialCache > LazyInstance { get ; } = new ( LoadOrCreate ) ;
72+ public static CredentialCache Instance
73+ {
74+ get
75+ {
76+ lock ( _lock )
77+ {
78+ if ( _instance is null )
79+ {
80+ _persistenceProvider ??= CreateDefaultPersistenceProvider ( ) ;
81+ _instance = new CredentialCache ( _persistenceProvider ) ;
82+ }
83+ return _instance ;
84+ }
85+ }
86+ }
3987
4088 /// <summary>
41- /// Retrieves the singleton instance of the <see cref="CredentialCache"/> class.
89+ /// Configures the persistence provider for the credential cache.
90+ /// This must be called before accessing the Instance property if you want to use a custom provider.
4291 /// </summary>
43- public static CredentialCache Instance => LazyInstance . Value ;
92+ /// <param name="persistenceProvider">The persistence provider to use.</param>
93+ public static void ConfigurePersistenceProvider ( IPersistenceProvider < string > persistenceProvider )
94+ {
95+ lock ( _lock )
96+ {
97+ if ( _instance is not null )
98+ {
99+ throw new InvalidOperationException ( "Cannot configure persistence provider after instance has been created. Call this method before accessing Instance." ) ;
100+ }
101+ _persistenceProvider = persistenceProvider ?? throw new ArgumentNullException ( nameof ( persistenceProvider ) ) ;
102+ }
103+ }
44104
45105 /// <summary>
46106 /// Tries to get a credential associated with the specified persona GUID.
@@ -49,18 +109,30 @@ public class CredentialCache : AppData<CredentialCache>
49109 /// <param name="gitCredential">When this method returns, contains the credential associated with the specified GUID, if the GUID is found; otherwise, null.</param>
50110 /// <returns><c>true</c> if the credential was found; otherwise, <c>false</c>.</returns>
51111 public bool TryGet ( PersonaGUID providerGuid , out Credential ? gitCredential ) =>
52- Credentials . TryGetValue ( providerGuid , out gitCredential ) ;
112+ Data . Credentials . TryGetValue ( providerGuid , out gitCredential ) ;
53113
54114 /// <summary>
55115 /// Adds or replaces a credential associated with the specified persona GUID.
56116 /// </summary>
57117 /// <param name="providerGuid">The GUID of the persona.</param>
58118 /// <param name="gitCredential">The credential to add or replace.</param>
59119 /// <exception cref="ArgumentNullException">Thrown when <paramref name="gitCredential"/> is null.</exception>
60- public void AddOrReplace ( PersonaGUID providerGuid , Credential gitCredential )
120+ public async Task AddOrReplaceAsync ( PersonaGUID providerGuid , Credential gitCredential , CancellationToken cancellationToken = default )
61121 {
62122 ArgumentNullException . ThrowIfNull ( gitCredential ) ;
63- Credentials [ providerGuid ] = gitCredential ;
123+ Data . Credentials [ providerGuid ] = gitCredential ;
124+ await SaveAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
125+ }
126+
127+ /// <summary>
128+ /// Adds or replaces a credential associated with the specified persona GUID synchronously.
129+ /// </summary>
130+ /// <param name="providerGuid">The GUID of the persona.</param>
131+ /// <param name="gitCredential">The credential to add or replace.</param>
132+ /// <exception cref="ArgumentNullException">Thrown when <paramref name="gitCredential"/> is null.</exception>
133+ public void AddOrReplace ( PersonaGUID providerGuid , Credential gitCredential )
134+ {
135+ AddOrReplaceAsync ( providerGuid , gitCredential ) . GetAwaiter ( ) . GetResult ( ) ;
64136 }
65137
66138 /// <summary>
@@ -108,5 +180,57 @@ public void RegisterCredentialFactory<T>(ICredentialFactory<T> factory) where T
108180 /// <typeparam name="T">The type of the credential.</typeparam>
109181 public void UnregisterCredentialFactory < T > ( ) where T : Credential =>
110182 CredentialFactories . TryRemove ( typeof ( T ) , out _ ) ;
183+
184+ /// <summary>
185+ /// Saves the current credential cache data to persistent storage.
186+ /// </summary>
187+ /// <param name="cancellationToken">A cancellation token to cancel the operation.</param>
188+ /// <returns>A task that represents the asynchronous save operation.</returns>
189+ public async Task SaveAsync ( CancellationToken cancellationToken = default )
190+ {
191+ await PersistenceProvider . StoreAsync ( CacheKey , Data , cancellationToken ) . ConfigureAwait ( false ) ;
192+ }
193+
194+ /// <summary>
195+ /// Saves the current credential cache data to persistent storage synchronously.
196+ /// </summary>
197+ public void Save ( )
198+ {
199+ SaveAsync ( ) . GetAwaiter ( ) . GetResult ( ) ;
200+ }
201+
202+ /// <summary>
203+ /// Loads or creates the credential cache data from persistent storage.
204+ /// </summary>
205+ /// <param name="cancellationToken">A cancellation token to cancel the operation.</param>
206+ /// <returns>The loaded or newly created credential cache data.</returns>
207+ private async Task < CredentialCacheData > LoadOrCreateData ( CancellationToken cancellationToken = default )
208+ {
209+ return await PersistenceProvider . RetrieveOrCreateAsync < CredentialCacheData > ( CacheKey , cancellationToken ) . ConfigureAwait ( false ) ;
210+ }
211+
212+ /// <summary>
213+ /// Creates the default persistence provider for the credential cache.
214+ /// </summary>
215+ /// <returns>A new instance of the default persistence provider.</returns>
216+ private static IPersistenceProvider < string > CreateDefaultPersistenceProvider ( )
217+ {
218+ var fileSystemProvider = new FileSystemProvider ( ) ;
219+ var jsonSerializer = new JsonSerializer ( ) ;
220+ var serializationProvider = new UniversalSerializationProvider ( jsonSerializer , providerName : "CredentialCache.Json" ) ;
221+ return new AppDataPersistenceProvider < string > (
222+ fileSystemProvider ,
223+ serializationProvider ,
224+ applicationName : "ktsu" ,
225+ subdirectory : "CredentialCache" ) ;
226+ }
227+
228+ /// <summary>
229+ /// Disposes the credential cache instance and saves any pending changes.
230+ /// </summary>
231+ public void Dispose ( )
232+ {
233+ Save ( ) ;
234+ }
111235}
112236
0 commit comments